diff --git a/scm-plugins/scm-git-plugin/package.json b/scm-plugins/scm-git-plugin/package.json index 6377574498..b617351264 100644 --- a/scm-plugins/scm-git-plugin/package.json +++ b/scm-plugins/scm-git-plugin/package.json @@ -12,6 +12,6 @@ "@scm-manager/ui-extensions": "^0.1.1" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.21" + "@scm-manager/ui-bundler": "^0.0.22" } } diff --git a/scm-plugins/scm-git-plugin/yarn.lock b/scm-plugins/scm-git-plugin/yarn.lock index 3514ed3f2c..471beb513e 100644 --- a/scm-plugins/scm-git-plugin/yarn.lock +++ b/scm-plugins/scm-git-plugin/yarn.lock @@ -707,9 +707,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.21": - version "0.0.21" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.21.tgz#f8b5fa355415cc67b8aaf8744e1701a299dff647" +"@scm-manager/ui-bundler@^0.0.22": + version "0.0.22" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.22.tgz#6eaed4e1f0b1fbc6ed1ebbf7eb0f5585f760949a" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" diff --git a/scm-plugins/scm-hg-plugin/package.json b/scm-plugins/scm-hg-plugin/package.json index dbca702070..6bccf3bd96 100644 --- a/scm-plugins/scm-hg-plugin/package.json +++ b/scm-plugins/scm-hg-plugin/package.json @@ -9,6 +9,6 @@ "@scm-manager/ui-extensions": "^0.1.1" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.21" + "@scm-manager/ui-bundler": "^0.0.22" } } diff --git a/scm-plugins/scm-hg-plugin/yarn.lock b/scm-plugins/scm-hg-plugin/yarn.lock index a211aa0ca1..bc53e0a7ce 100644 --- a/scm-plugins/scm-hg-plugin/yarn.lock +++ b/scm-plugins/scm-hg-plugin/yarn.lock @@ -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.21": - version "0.0.21" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.21.tgz#f8b5fa355415cc67b8aaf8744e1701a299dff647" +"@scm-manager/ui-bundler@^0.0.22": + version "0.0.22" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.22.tgz#6eaed4e1f0b1fbc6ed1ebbf7eb0f5585f760949a" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" diff --git a/scm-plugins/scm-svn-plugin/package.json b/scm-plugins/scm-svn-plugin/package.json index 41f1c88a18..3e509172fd 100644 --- a/scm-plugins/scm-svn-plugin/package.json +++ b/scm-plugins/scm-svn-plugin/package.json @@ -9,6 +9,6 @@ "@scm-manager/ui-extensions": "^0.1.1" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.21" + "@scm-manager/ui-bundler": "^0.0.22" } } diff --git a/scm-plugins/scm-svn-plugin/yarn.lock b/scm-plugins/scm-svn-plugin/yarn.lock index a211aa0ca1..bc53e0a7ce 100644 --- a/scm-plugins/scm-svn-plugin/yarn.lock +++ b/scm-plugins/scm-svn-plugin/yarn.lock @@ -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.21": - version "0.0.21" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.21.tgz#f8b5fa355415cc67b8aaf8744e1701a299dff647" +"@scm-manager/ui-bundler@^0.0.22": + version "0.0.22" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.22.tgz#6eaed4e1f0b1fbc6ed1ebbf7eb0f5585f760949a" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" diff --git a/scm-ui-components/packages/ui-components/package.json b/scm-ui-components/packages/ui-components/package.json index 4a4b4dc82e..880d8f3891 100644 --- a/scm-ui-components/packages/ui-components/package.json +++ b/scm-ui-components/packages/ui-components/package.json @@ -14,7 +14,7 @@ "eslint-fix": "eslint src --fix" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.21", + "@scm-manager/ui-bundler": "^0.0.22", "create-index": "^2.3.0", "enzyme": "^3.5.0", "enzyme-adapter-react-16": "^1.3.1", @@ -34,7 +34,8 @@ "react-dom": "^16.5.2", "react-i18next": "^7.11.0", "react-jss": "^8.6.1", - "react-router-dom": "^4.3.1" + "react-router-dom": "^4.3.1", + "react-select": "^2.1.2" }, "browserify": { "transform": [ diff --git a/scm-ui-components/packages/ui-components/src/Autocomplete.js b/scm-ui-components/packages/ui-components/src/Autocomplete.js new file mode 100644 index 0000000000..f3023e268b --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/Autocomplete.js @@ -0,0 +1,73 @@ +// @flow +import React from "react"; +import { AsyncCreatable } from "react-select"; +import type { AutocompleteObject, SelectValue } from "@scm-manager/ui-types"; +import LabelWithHelpIcon from "./forms/LabelWithHelpIcon"; + + +type Props = { + loadSuggestions: string => Promise, + valueSelected: SelectValue => void, + label: string, + helpText?: string, + value?: SelectValue, + placeholder: string, + loadingMessage: string, + noOptionsMessage: string +}; + + +type State = {}; + +class Autocomplete extends React.Component { + + + static defaultProps = { + placeholder: "Type here", + loadingMessage: "Loading...", + noOptionsMessage: "No suggestion available" + }; + + handleInputChange = (newValue: SelectValue) => { + this.props.valueSelected(newValue); + }; + + // We overwrite this to avoid running into a bug (https://github.com/JedWatson/react-select/issues/2944) + isValidNewOption = (inputValue: string, selectValue: SelectValue, selectOptions: SelectValue[]) => { + const isNotDuplicated = !selectOptions + .map(option => option.label) + .includes(inputValue); + const isNotEmpty = inputValue !== ""; + return isNotEmpty && isNotDuplicated; + }; + + render() { + const { label, helpText, value, placeholder, loadingMessage, noOptionsMessage, loadSuggestions } = this.props; + return ( +
+ +
+ loadingMessage} + noOptionsMessage={() => noOptionsMessage} + isValidNewOption={this.isValidNewOption} + onCreateOption={value => { + this.handleInputChange({ + label: value, + value: { id: value, displayName: value } + }); + }} + /> +
+
+ ); + } +} + + +export default Autocomplete; diff --git a/scm-ui-components/packages/ui-components/src/ErrorNotification.js b/scm-ui-components/packages/ui-components/src/ErrorNotification.js index 9ef3b58653..5600d81799 100644 --- a/scm-ui-components/packages/ui-components/src/ErrorNotification.js +++ b/scm-ui-components/packages/ui-components/src/ErrorNotification.js @@ -18,7 +18,7 @@ class ErrorNotification extends React.Component { ); } - return ""; + return null; } } diff --git a/scm-ui-components/packages/ui-components/src/Paginator.test.js b/scm-ui-components/packages/ui-components/src/Paginator.test.js index 1d32e22faf..d32b4df702 100644 --- a/scm-ui-components/packages/ui-components/src/Paginator.test.js +++ b/scm-ui-components/packages/ui-components/src/Paginator.test.js @@ -1,12 +1,11 @@ // @flow import React from "react"; -import {mount, shallow} from "enzyme"; +import { mount, shallow } from "enzyme"; import "./tests/enzyme"; import "./tests/i18n"; import ReactRouterEnzymeContext from "react-router-enzyme-context"; import Paginator from "./Paginator"; -// TODO: Fix tests xdescribe("paginator rendering tests", () => { const options = new ReactRouterEnzymeContext(); diff --git a/scm-ui-components/packages/ui-components/src/forms/AutocompleteAddEntryToTableField.js b/scm-ui-components/packages/ui-components/src/forms/AutocompleteAddEntryToTableField.js new file mode 100644 index 0000000000..f3af79e658 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/forms/AutocompleteAddEntryToTableField.js @@ -0,0 +1,88 @@ +//@flow +import React from "react"; + +import type { AutocompleteObject, SelectValue } from "@scm-manager/ui-types"; +import Autocomplete from "../Autocomplete"; +import AddButton from "../buttons/AddButton"; + +type Props = { + addEntry: SelectValue => void, + disabled: boolean, + buttonLabel: string, + fieldLabel: string, + helpText?: string, + loadSuggestions: string => Promise, + placeholder?: string, + loadingMessage?: string, + noOptionsMessage?: string +}; + +type State = { + selectedValue?: SelectValue +}; + +class AutocompleteAddEntryToTableField extends React.Component { + constructor(props: Props) { + super(props); + this.state = { selectedValue: undefined }; + } + render() { + const { + disabled, + buttonLabel, + fieldLabel, + helpText, + loadSuggestions, + placeholder, + loadingMessage, + noOptionsMessage + } = this.props; + + const { selectedValue } = this.state; + return ( +
+ + + +
+ ); + } + + addButtonClicked = (event: Event) => { + event.preventDefault(); + this.appendEntry(); + }; + + appendEntry = () => { + const { selectedValue } = this.state; + if (!selectedValue) { + return; + } + // $FlowFixMe null is needed to clear the selection; undefined does not work + this.setState({ ...this.state, selectedValue: null }, () => + this.props.addEntry(selectedValue) + ); + }; + + handleAddEntryChange = (selection: SelectValue) => { + this.setState({ + ...this.state, + selectedValue: selection + }); + }; +} + +export default AutocompleteAddEntryToTableField; diff --git a/scm-ui-components/packages/ui-components/src/forms/LabelWithHelpIcon.js b/scm-ui-components/packages/ui-components/src/forms/LabelWithHelpIcon.js index 8a917828ec..c0e1dffb6a 100644 --- a/scm-ui-components/packages/ui-components/src/forms/LabelWithHelpIcon.js +++ b/scm-ui-components/packages/ui-components/src/forms/LabelWithHelpIcon.js @@ -1,6 +1,6 @@ //@flow import React from "react"; -import Help from '../Help'; +import Help from "../Help.js"; type Props = { label?: string, diff --git a/scm-ui-components/packages/ui-components/src/forms/PasswordConfirmation.js b/scm-ui-components/packages/ui-components/src/forms/PasswordConfirmation.js index 3dc59ad906..b0f53cddeb 100644 --- a/scm-ui-components/packages/ui-components/src/forms/PasswordConfirmation.js +++ b/scm-ui-components/packages/ui-components/src/forms/PasswordConfirmation.js @@ -11,7 +11,7 @@ type State = { passwordConfirmationFailed: boolean }; type Props = { - passwordChanged: string => void, + passwordChanged: (string, boolean) => void, passwordValidator?: string => boolean, // Context props t: string => string @@ -98,14 +98,12 @@ class PasswordConfirmation extends React.Component { ); }; + isValid = () => { + return this.state.passwordValid && !this.state.passwordConfirmationFailed + }; + propagateChange = () => { - if ( - this.state.password && - this.state.passwordValid && - !this.state.passwordConfirmationFailed - ) { - this.props.passwordChanged(this.state.password); - } + this.props.passwordChanged(this.state.password, this.isValid()); }; } diff --git a/scm-ui-components/packages/ui-components/src/forms/index.js b/scm-ui-components/packages/ui-components/src/forms/index.js index b1cf06740f..714b9b3301 100644 --- a/scm-ui-components/packages/ui-components/src/forms/index.js +++ b/scm-ui-components/packages/ui-components/src/forms/index.js @@ -1,6 +1,7 @@ // @create-index export { default as AddEntryToTableField } from "./AddEntryToTableField.js"; +export { default as AutocompleteAddEntryToTableField } from "./AutocompleteAddEntryToTableField.js"; export { default as Checkbox } from "./Checkbox.js"; export { default as InputField } from "./InputField.js"; export { default as Select } from "./Select.js"; diff --git a/scm-ui-components/packages/ui-components/src/index.js b/scm-ui-components/packages/ui-components/src/index.js index 0900af2190..334e0a696c 100644 --- a/scm-ui-components/packages/ui-components/src/index.js +++ b/scm-ui-components/packages/ui-components/src/index.js @@ -23,6 +23,7 @@ export { default as Help } from "./Help"; export { default as HelpIcon } from "./HelpIcon"; export { default as Tooltip } from "./Tooltip"; export { getPageFromMatch } from "./urls"; +export { default as Autocomplete} from "./Autocomplete"; export { apiClient, NOT_FOUND_ERROR_MESSAGE, UNAUTHORIZED_ERROR_MESSAGE } from "./apiclient.js"; diff --git a/scm-ui-components/packages/ui-components/yarn.lock b/scm-ui-components/packages/ui-components/yarn.lock index 94816787ec..743e46f123 100644 --- a/scm-ui-components/packages/ui-components/yarn.lock +++ b/scm-ui-components/packages/ui-components/yarn.lock @@ -576,6 +576,12 @@ "@babel/plugin-transform-react-jsx-self" "^7.0.0" "@babel/plugin-transform-react-jsx-source" "^7.0.0" +"@babel/runtime@^7.1.2": + version "7.1.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.1.5.tgz#4170907641cf1f61508f563ece3725150cc6fe39" + dependencies: + regenerator-runtime "^0.12.0" + "@babel/template@^7.1.0", "@babel/template@^7.1.2": version "7.1.2" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.1.2.tgz#090484a574fef5a2d2d7726a674eceda5c5b5644" @@ -606,6 +612,46 @@ lodash "^4.17.10" to-fast-properties "^2.0.0" +"@emotion/babel-utils@^0.6.4": + version "0.6.10" + resolved "https://registry.yarnpkg.com/@emotion/babel-utils/-/babel-utils-0.6.10.tgz#83dbf3dfa933fae9fc566e54fbb45f14674c6ccc" + dependencies: + "@emotion/hash" "^0.6.6" + "@emotion/memoize" "^0.6.6" + "@emotion/serialize" "^0.9.1" + convert-source-map "^1.5.1" + find-root "^1.1.0" + source-map "^0.7.2" + +"@emotion/hash@^0.6.2", "@emotion/hash@^0.6.6": + version "0.6.6" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.6.6.tgz#62266c5f0eac6941fece302abad69f2ee7e25e44" + +"@emotion/memoize@^0.6.1", "@emotion/memoize@^0.6.6": + version "0.6.6" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.6.6.tgz#004b98298d04c7ca3b4f50ca2035d4f60d2eed1b" + +"@emotion/serialize@^0.9.1": + version "0.9.1" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.9.1.tgz#a494982a6920730dba6303eb018220a2b629c145" + dependencies: + "@emotion/hash" "^0.6.6" + "@emotion/memoize" "^0.6.6" + "@emotion/unitless" "^0.6.7" + "@emotion/utils" "^0.8.2" + +"@emotion/stylis@^0.7.0": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.7.1.tgz#50f63225e712d99e2b2b39c19c70fff023793ca5" + +"@emotion/unitless@^0.6.2", "@emotion/unitless@^0.6.7": + version "0.6.7" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.6.7.tgz#53e9f1892f725b194d5e6a1684a7b394df592397" + +"@emotion/utils@^0.8.2": + version "0.8.2" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.8.2.tgz#576ff7fb1230185b619a75d258cbc98f0867a8dc" + "@gulp-sourcemaps/identity-map@1.X": version "1.0.2" resolved "https://registry.yarnpkg.com/@gulp-sourcemaps/identity-map/-/identity-map-1.0.2.tgz#1e6fe5d8027b1f285dc0d31762f566bccd73d5a9" @@ -641,9 +687,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.21": - version "0.0.21" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.21.tgz#f8b5fa355415cc67b8aaf8744e1701a299dff647" +"@scm-manager/ui-bundler@^0.0.22": + version "0.0.22" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.22.tgz#6eaed4e1f0b1fbc6ed1ebbf7eb0f5585f760949a" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" @@ -1121,6 +1167,23 @@ babel-messages@^6.23.0: dependencies: babel-runtime "^6.22.0" +babel-plugin-emotion@^9.2.11: + version "9.2.11" + resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-9.2.11.tgz#319c005a9ee1d15bb447f59fe504c35fd5807728" + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@emotion/babel-utils" "^0.6.4" + "@emotion/hash" "^0.6.2" + "@emotion/memoize" "^0.6.1" + "@emotion/stylis" "^0.7.0" + babel-plugin-macros "^2.0.0" + babel-plugin-syntax-jsx "^6.18.0" + convert-source-map "^1.5.0" + find-root "^1.1.0" + mkdirp "^0.5.1" + source-map "^0.5.7" + touch "^2.0.1" + babel-plugin-istanbul@^4.1.6: version "4.1.6" resolved "http://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz#36c59b2192efce81c5b378321b74175add1c9a45" @@ -1134,6 +1197,17 @@ babel-plugin-jest-hoist@^23.2.0: version "23.2.0" resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-23.2.0.tgz#e61fae05a1ca8801aadee57a6d66b8cefaf44167" +babel-plugin-macros@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.4.2.tgz#21b1a2e82e2130403c5ff785cba6548e9b644b28" + dependencies: + cosmiconfig "^5.0.5" + resolve "^1.8.1" + +babel-plugin-syntax-jsx@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" + babel-plugin-syntax-object-rest-spread@^6.13.0: version "6.13.0" resolved "http://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" @@ -1633,12 +1707,24 @@ cached-path-relative@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.1.tgz#d09c4b52800aa4c078e2dd81a869aac90d2e54e7" +caller-callsite@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" + dependencies: + callsites "^2.0.0" + caller-path@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" dependencies: callsites "^0.2.0" +caller-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" + dependencies: + caller-callsite "^2.0.0" + callsite@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" @@ -1782,7 +1868,7 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -classnames@^2.2.6: +classnames@^2.2.5, classnames@^2.2.6: version "2.2.6" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" @@ -1966,7 +2052,7 @@ contains-path@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" -convert-source-map@1.X, convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.5.1: +convert-source-map@1.X, convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.5.1: version "1.6.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" dependencies: @@ -1992,6 +2078,15 @@ core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" +cosmiconfig@^5.0.5: + version "5.0.7" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.0.7.tgz#39826b292ee0d78eda137dfa3173bd1c21a43b04" + dependencies: + import-fresh "^2.0.0" + is-directory "^0.3.1" + js-yaml "^3.9.0" + parse-json "^4.0.0" + coveralls@^2.11.3: version "2.13.3" resolved "https://registry.yarnpkg.com/coveralls/-/coveralls-2.13.3.tgz#9ad7c2ae527417f361e8b626483f48ee92dd2bc7" @@ -2009,6 +2104,18 @@ create-ecdh@^4.0.0: bn.js "^4.1.0" elliptic "^6.0.0" +create-emotion@^9.2.12: + version "9.2.12" + resolved "https://registry.yarnpkg.com/create-emotion/-/create-emotion-9.2.12.tgz#0fc8e7f92c4f8bb924b0fef6781f66b1d07cb26f" + dependencies: + "@emotion/hash" "^0.6.2" + "@emotion/memoize" "^0.6.1" + "@emotion/stylis" "^0.7.0" + "@emotion/unitless" "^0.6.2" + csstype "^2.5.2" + stylis "^3.5.0" + stylis-rule-sheet "^0.0.10" + create-hash@^1.1.0, create-hash@^1.1.2: version "1.2.0" resolved "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" @@ -2122,6 +2229,10 @@ cssstyle@^1.0.0: dependencies: cssom "0.3.x" +csstype@^2.5.2: + version "2.5.7" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.5.7.tgz#bf9235d5872141eccfb2d16d82993c6b149179ff" + d@1: version "1.0.0" resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" @@ -2362,6 +2473,12 @@ doctrine@^2.1.0: dependencies: esutils "^2.0.2" +dom-helpers@^3.3.1: + version "3.4.0" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8" + dependencies: + "@babel/runtime" "^7.1.2" + dom-serializer@0, dom-serializer@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" @@ -2466,6 +2583,13 @@ emoji-regex@^6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.5.1.tgz#9baea929b155565c11ea41c6626eaa65cef992c2" +emotion@^9.1.2: + version "9.2.12" + resolved "https://registry.yarnpkg.com/emotion/-/emotion-9.2.12.tgz#53925aaa005614e65c6e43db8243c843574d1ea9" + dependencies: + babel-plugin-emotion "^9.2.11" + create-emotion "^9.2.12" + encodeurl@~1.0.1, encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" @@ -2561,7 +2685,7 @@ enzyme@^3.5.0: rst-selector-parser "^2.2.3" string.prototype.trim "^1.1.2" -error-ex@^1.2.0: +error-ex@^1.2.0, error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" dependencies: @@ -3070,6 +3194,10 @@ find-node-modules@^1.0.4: findup-sync "0.4.2" merge "^1.2.0" +find-root@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" + find-up@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" @@ -3862,6 +3990,13 @@ immutable@^3: version "3.8.2" resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3" +import-fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" + dependencies: + caller-path "^2.0.0" + resolve-from "^3.0.0" + import-local@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/import-local/-/import-local-1.0.0.tgz#5e4ffdc03f4fe6c009c6729beb29631c2f8227bc" @@ -4038,6 +4173,10 @@ is-descriptor@^1.0.0, is-descriptor@^1.0.2: is-data-descriptor "^1.0.0" kind-of "^6.0.2" +is-directory@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" + is-dotfile@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" @@ -4689,7 +4828,7 @@ js-yaml@3.6.1: argparse "^1.0.7" esprima "^2.6.0" -js-yaml@^3.12.0, js-yaml@^3.7.0: +js-yaml@^3.12.0, js-yaml@^3.7.0, js-yaml@^3.9.0: version "3.12.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" dependencies: @@ -4743,6 +4882,10 @@ jsesc@~0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" +json-parse-better-errors@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + json-schema-traverse@^0.3.0: version "0.3.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" @@ -5127,7 +5270,7 @@ log-driver@1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/log-driver/-/log-driver-1.2.5.tgz#7ae4ec257302fd790d557cb10c97100d857b0056" -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" dependencies: @@ -5216,6 +5359,10 @@ mem@^1.1.0: dependencies: mimic-fn "^1.0.0" +memoize-one@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.0.3.tgz#cdfdd942853f1a1b4c71c5336b8c49da0bf0273c" + memoizee@0.4.X: version "0.4.14" resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.14.tgz#07a00f204699f9a95c2d9e77218271c7cd610d57" @@ -5556,6 +5703,12 @@ nopt@^4.0.1: abbrev "1" osenv "^0.1.4" +nopt@~1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" + dependencies: + abbrev "1" + normalize-package-data@^2.3.2: version "2.4.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" @@ -5907,6 +6060,13 @@ parse-json@^2.2.0: dependencies: error-ex "^1.2.0" +parse-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + dependencies: + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + parse-passwd@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" @@ -6279,6 +6439,12 @@ react-i18next@^7.11.0: html-parse-stringify2 "2.0.1" prop-types "^15.6.0" +react-input-autosize@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.1.tgz#ec428fa15b1592994fb5f9aa15bb1eb6baf420f8" + dependencies: + prop-types "^15.5.8" + react-is@^16.5.2: version "16.5.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.5.2.tgz#e2a7b7c3f5d48062eb769fcb123505eb928722e3" @@ -6293,6 +6459,10 @@ react-jss@^8.6.1: prop-types "^15.6.0" theming "^1.3.0" +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + react-router-dom@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.3.1.tgz#4c2619fc24c4fa87c9fd18f4fb4a43fe63fbd5c6" @@ -6323,6 +6493,18 @@ react-router@^4.3.1: prop-types "^15.6.1" warning "^4.0.1" +react-select@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/react-select/-/react-select-2.1.2.tgz#7a3e4c2b9efcd8c44ae7cf6ebb8b060ef69c513c" + dependencies: + classnames "^2.2.5" + emotion "^9.1.2" + memoize-one "^4.0.0" + prop-types "^15.6.0" + raf "^3.4.0" + react-input-autosize "^2.2.1" + react-transition-group "^2.2.1" + react-test-renderer@^16.0.0-0: version "16.5.2" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.5.2.tgz#92e9d2c6f763b9821b2e0b22f994ee675068b5ae" @@ -6332,6 +6514,15 @@ react-test-renderer@^16.0.0-0: react-is "^16.5.2" schedule "^0.5.0" +react-transition-group@^2.2.1: + version "2.5.0" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.5.0.tgz#70bca0e3546102c4dc5cf3f5f57f73447cce6874" + dependencies: + dom-helpers "^3.3.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react-lifecycles-compat "^3.0.4" + react@^16.4.2: version "16.6.0" resolved "https://registry.yarnpkg.com/react/-/react-16.6.0.tgz#b34761cfaf3e30f5508bc732fb4736730b7da246" @@ -6466,6 +6657,10 @@ regenerator-runtime@^0.11.0: version "0.11.1" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" +regenerator-runtime@^0.12.0: + version "0.12.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" + regenerator-transform@^0.13.3: version "0.13.3" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.3.tgz#264bd9ff38a8ce24b06e0636496b2c856b57bcbb" @@ -6659,7 +6854,7 @@ resolve@1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" -resolve@^1.1.4, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.6.0: +resolve@^1.1.4, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.6.0, resolve@^1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26" dependencies: @@ -7033,6 +7228,10 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" +source-map@^0.7.2: + version "0.7.3" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" + sparkles@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.1.tgz#008db65edce6c50eec0c5e228e1945061dd0437c" @@ -7243,6 +7442,14 @@ strip-json-comments@^2.0.1, strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" +stylis-rule-sheet@^0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz#44e64a2b076643f4b52e5ff71efc04d8c3c4a430" + +stylis@^3.5.0: + version "3.5.4" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.4.tgz#f665f25f5e299cf3d64654ab949a57c768b73fbe" + subarg@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2" @@ -7442,6 +7649,12 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" +touch@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/touch/-/touch-2.0.2.tgz#ca0b2a3ae3211246a61b16ba9e6cbf1596287164" + dependencies: + nopt "~1.0.10" + tough-cookie@>=2.3.3, tough-cookie@^2.3.4, tough-cookie@~2.4.3: version "2.4.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" diff --git a/scm-ui-components/packages/ui-types/package.json b/scm-ui-components/packages/ui-types/package.json index 78452c2ef5..8a009d314c 100644 --- a/scm-ui-components/packages/ui-types/package.json +++ b/scm-ui-components/packages/ui-types/package.json @@ -14,7 +14,7 @@ "check": "flow check" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.21" + "@scm-manager/ui-bundler": "^0.0.22" }, "browserify": { "transform": [ @@ -33,4 +33,4 @@ ] ] } -} \ No newline at end of file +} diff --git a/scm-ui-components/packages/ui-types/src/Autocomplete.js b/scm-ui-components/packages/ui-types/src/Autocomplete.js new file mode 100644 index 0000000000..407108c115 --- /dev/null +++ b/scm-ui-components/packages/ui-types/src/Autocomplete.js @@ -0,0 +1,10 @@ +// @flow +export type AutocompleteObject = { + id: string, + displayName: string +}; + +export type SelectValue = { + value: AutocompleteObject, + label: string +}; diff --git a/scm-ui-components/packages/ui-types/src/index.js b/scm-ui-components/packages/ui-types/src/index.js index 883272b4d4..cf739f747d 100644 --- a/scm-ui-components/packages/ui-types/src/index.js +++ b/scm-ui-components/packages/ui-types/src/index.js @@ -22,3 +22,5 @@ export type { IndexResources } from "./IndexResources"; export type { Permission, PermissionCreateEntry, PermissionCollection } from "./RepositoryPermissions"; export type { SubRepository, File } from "./Sources"; + +export type { SelectValue, AutocompleteObject } from "./Autocomplete"; diff --git a/scm-ui-components/packages/ui-types/yarn.lock b/scm-ui-components/packages/ui-types/yarn.lock index fe2df2f76a..a19d99dfbf 100644 --- a/scm-ui-components/packages/ui-types/yarn.lock +++ b/scm-ui-components/packages/ui-types/yarn.lock @@ -707,9 +707,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.21": - version "0.0.21" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.21.tgz#f8b5fa355415cc67b8aaf8744e1701a299dff647" +"@scm-manager/ui-bundler@^0.0.22": + version "0.0.22" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.22.tgz#6eaed4e1f0b1fbc6ed1ebbf7eb0f5585f760949a" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" diff --git a/scm-ui/package.json b/scm-ui/package.json index d80ee6571e..6fd0febd9c 100644 --- a/scm-ui/package.json +++ b/scm-ui/package.json @@ -28,6 +28,7 @@ "react-redux": "^5.0.7", "react-router-dom": "^4.3.1", "react-router-redux": "^5.0.0-alpha.9", + "react-select": "^2.1.2", "react-syntax-highlighter": "^9.0.1", "redux": "^4.0.0", "redux-devtools-extension": "^2.13.5", @@ -51,7 +52,7 @@ "pre-commit": "jest && flow && eslint src" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.21", + "@scm-manager/ui-bundler": "^0.0.22", "concat": "^1.0.3", "copyfiles": "^2.0.0", "enzyme": "^3.3.0", diff --git a/scm-ui/public/locales/en/groups.json b/scm-ui/public/locales/en/groups.json index daa2cc651a..f1ebb95e18 100644 --- a/scm-ui/public/locales/en/groups.json +++ b/scm-ui/public/locales/en/groups.json @@ -39,7 +39,13 @@ "label": "Add member", "error": "Invalid member name" }, - "group-form": { + "add-member-autocomplete": { + "placeholder": "Enter member", + "loading": "Loading...", + "no-options": "No suggestion available" + }, + +"group-form": { "submit": "Submit", "name-error": "Group name is invalid", "description-error": "Description is invalid", diff --git a/scm-ui/public/locales/en/repos.json b/scm-ui/public/locales/en/repos.json index 7e421e7b99..8296b46907 100644 --- a/scm-ui/public/locales/en/repos.json +++ b/scm-ui/public/locales/en/repos.json @@ -84,11 +84,14 @@ "label": "Branches" }, "permission": { + "user": "User", + "group": "Group", "error-title": "Error", "error-subtitle": "Unknown permissions error", "name": "User or Group", "type": "Type", "group-permission": "Group Permission", + "user-permission": "User Permission", "edit-permission": { "delete-button": "Delete", "save-button": "Save Changes" @@ -111,6 +114,13 @@ "groupPermissionHelpText": "States if a permission is a group permission.", "nameHelpText": "Manage permissions for a specific user or group", "typeHelpText": "READ = read; WRITE = read and write; OWNER = read, write and also the ability to manage the properties and permissions" + }, + "autocomplete": { + "no-group-options": "No group suggestion available", + "group-placeholder": "Enter group", + "no-user-options": "No user suggestion available", + "user-placeholder": "Enter user", + "loading": "Loading..." } }, "help": { diff --git a/scm-ui/src/containers/ChangeUserPassword.js b/scm-ui/src/containers/ChangeUserPassword.js index 6fa38d470f..28a7af588a 100644 --- a/scm-ui/src/containers/ChangeUserPassword.js +++ b/scm-ui/src/containers/ChangeUserPassword.js @@ -21,7 +21,8 @@ type State = { password: string, loading: boolean, error?: Error, - passwordChanged: boolean + passwordChanged: boolean, + passwordValid: boolean }; class ChangeUserPassword extends React.Component { @@ -35,7 +36,8 @@ class ChangeUserPassword extends React.Component { passwordConfirmationError: false, validatePasswordError: false, validatePassword: "", - passwordChanged: false + passwordChanged: false, + passwordValid: false }; } @@ -83,6 +85,10 @@ class ChangeUserPassword extends React.Component { } }; + isValid = () => { + return this.state.oldPassword && this.state.passwordValid; + }; + render() { const { t } = this.props; const { loading, passwordChanged, error } = this.state; @@ -118,7 +124,7 @@ class ChangeUserPassword extends React.Component { key={this.state.passwordChanged ? "changed" : "unchanged"} /> @@ -126,8 +132,8 @@ class ChangeUserPassword extends React.Component { ); } - passwordChanged = (password: string) => { - this.setState({ ...this.state, password }); + passwordChanged = (password: string, passwordValid: boolean) => { + this.setState({ ...this.state, password, passwordValid: (!!password && passwordValid) }); }; onClose = () => { diff --git a/scm-ui/src/groups/components/GroupForm.js b/scm-ui/src/groups/components/GroupForm.js index 4958fbf0fa..589914021c 100644 --- a/scm-ui/src/groups/components/GroupForm.js +++ b/scm-ui/src/groups/components/GroupForm.js @@ -2,12 +2,12 @@ import React from "react"; import { translate } from "react-i18next"; import { + AutocompleteAddEntryToTableField, InputField, SubmitButton, - Textarea, - AddEntryToTableField + Textarea } from "@scm-manager/ui-components"; -import type { Group } from "@scm-manager/ui-types"; +import type { Group, SelectValue } from "@scm-manager/ui-types"; import * as validator from "./groupValidation"; import MemberNameTable from "./MemberNameTable"; @@ -16,7 +16,8 @@ type Props = { t: string => string, submitForm: Group => void, loading?: boolean, - group?: Group + group?: Group, + loadUserSuggestions: string => any }; type State = { @@ -70,7 +71,7 @@ class GroupForm extends React.Component { render() { const { t, loading } = this.props; - const group = this.state.group; + const { group } = this.state; let nameField = null; if (!this.props.group) { nameField = ( @@ -97,15 +98,20 @@ class GroupForm extends React.Component { helpText={t("group-form.help.descriptionHelpText")} /> - { }); }; - addMember = (membername: string) => { - if (this.isMember(membername)) { + addMember = (value: SelectValue) => { + if (this.isMember(value.value.id)) { return; } @@ -135,7 +141,7 @@ class GroupForm extends React.Component { ...this.state, group: { ...this.state.group, - members: [...this.state.group.members, membername] + members: [...this.state.group.members, value.value.id] } }); }; diff --git a/scm-ui/src/groups/containers/AddGroup.js b/scm-ui/src/groups/containers/AddGroup.js index 9b13ac0309..c19f6156d1 100644 --- a/scm-ui/src/groups/containers/AddGroup.js +++ b/scm-ui/src/groups/containers/AddGroup.js @@ -13,7 +13,10 @@ import { } from "../modules/groups"; import type { Group } from "@scm-manager/ui-types"; import type { History } from "history"; -import { getGroupsLink } from "../../modules/indexResource"; +import { + getGroupsLink, + getUserAutoCompleteLink +} from "../../modules/indexResource"; type Props = { t: string => string, @@ -22,7 +25,8 @@ type Props = { loading?: boolean, error?: Error, resetForm: () => void, - createLink: string + createLink: string, + autocompleteLink: string }; type State = {}; @@ -31,6 +35,7 @@ class AddGroup extends React.Component { componentDidMount() { this.props.resetForm(); } + render() { const { t, loading, error } = this.props; return ( @@ -43,12 +48,26 @@ class AddGroup extends React.Component { this.createGroup(group)} loading={loading} + loadUserSuggestions={this.loadUserAutocompletion} /> ); } + loadUserAutocompletion = (inputValue: string) => { + const url = this.props.autocompleteLink + "?q="; + return fetch(url + inputValue) + .then(response => response.json()) + .then(json => { + return json.map(element => { + return { + value: element, + label: `${element.displayName} (${element.id})` + }; + }); + }); + }; groupCreated = () => { this.props.history.push("/groups"); }; @@ -71,10 +90,12 @@ const mapStateToProps = state => { const loading = isCreateGroupPending(state); const error = getCreateGroupFailure(state); const createLink = getGroupsLink(state); + const autocompleteLink = getUserAutoCompleteLink(state); return { createLink, loading, - error + error, + autocompleteLink }; }; diff --git a/scm-ui/src/groups/containers/EditGroup.js b/scm-ui/src/groups/containers/EditGroup.js index ac6e737dac..223ea1eef6 100644 --- a/scm-ui/src/groups/containers/EditGroup.js +++ b/scm-ui/src/groups/containers/EditGroup.js @@ -3,21 +3,23 @@ import React from "react"; import { connect } from "react-redux"; import GroupForm from "../components/GroupForm"; import { - modifyGroup, - modifyGroupReset, + getModifyGroupFailure, isModifyGroupPending, - getModifyGroupFailure + modifyGroup, + modifyGroupReset } from "../modules/groups"; import type { History } from "history"; import { withRouter } from "react-router-dom"; import type { Group } from "@scm-manager/ui-types"; import { ErrorNotification } from "@scm-manager/ui-components"; +import { getUserAutoCompleteLink } from "../../modules/indexResource"; type Props = { group: Group, modifyGroup: (group: Group, callback?: () => void) => void, modifyGroupReset: Group => void, fetchGroup: (name: string) => void, + autocompleteLink: string, history: History, loading?: boolean, error: Error @@ -37,6 +39,20 @@ class EditGroup extends React.Component { this.props.modifyGroup(group, this.groupModified(group)); }; + loadUserAutocompletion = (inputValue: string) => { + const url = this.props.autocompleteLink + "?q="; + return fetch(url + inputValue) + .then(response => response.json()) + .then(json => { + return json.map(element => { + return { + value: element, + label: `${element.displayName} (${element.id})` + }; + }); + }); + }; + render() { const { group, loading, error } = this.props; return ( @@ -48,6 +64,7 @@ class EditGroup extends React.Component { this.modifyGroup(group); }} loading={loading} + loadUserSuggestions={this.loadUserAutocompletion} /> ); @@ -57,9 +74,11 @@ class EditGroup extends React.Component { const mapStateToProps = (state, ownProps) => { const loading = isModifyGroupPending(state, ownProps.group.name); const error = getModifyGroupFailure(state, ownProps.group.name); + const autocompleteLink = getUserAutoCompleteLink(state); return { loading, - error + error, + autocompleteLink }; }; diff --git a/scm-ui/src/modules/indexResource.js b/scm-ui/src/modules/indexResource.js index 98dd9848dc..df55c63756 100644 --- a/scm-ui/src/modules/indexResource.js +++ b/scm-ui/src/modules/indexResource.js @@ -2,7 +2,7 @@ import * as types from "./types"; import { apiClient } from "@scm-manager/ui-components"; -import type { Action, IndexResources } from "@scm-manager/ui-types"; +import type { Action, IndexResources, Link } from "@scm-manager/ui-types"; import { isPending } from "./pending"; import { getFailure } from "./failure"; @@ -100,6 +100,13 @@ export function getLink(state: Object, name: string) { } } +export function getLinkCollection(state: Object, name: string): Link[] { + if (state.indexResources.links && state.indexResources.links[name]) { + return state.indexResources.links[name]; + } + return []; +} + export function getUiPluginsLink(state: Object) { return getLink(state, "uiPlugins"); } @@ -143,3 +150,23 @@ export function getGitConfigLink(state: Object) { export function getSvnConfigLink(state: Object) { return getLink(state, "svnConfig"); } + +export function getUserAutoCompleteLink(state: Object): string { + const link = getLinkCollection(state, "autocomplete").find( + i => i.name === "users" + ); + if (link) { + return link.href; + } + return ""; +} + +export function getGroupAutoCompleteLink(state: Object): string { + const link = getLinkCollection(state, "autocomplete").find( + i => i.name === "groups" + ); + if (link) { + return link.href; + } + return ""; +} diff --git a/scm-ui/src/modules/indexResource.test.js b/scm-ui/src/modules/indexResource.test.js index 2199da8290..ed3b7cb4d5 100644 --- a/scm-ui/src/modules/indexResource.test.js +++ b/scm-ui/src/modules/indexResource.test.js @@ -20,7 +20,11 @@ import reducer, { getHgConfigLink, getGitConfigLink, getSvnConfigLink, - getLinks, getGroupsLink + getLinks, + getGroupsLink, + getLinkCollection, + getUserAutoCompleteLink, + getGroupAutoCompleteLink } from "./indexResource"; const indexResourcesUnauthenticated = { @@ -73,354 +77,404 @@ const indexResourcesAuthenticated = { }, svnConfig: { href: "http://localhost:8081/scm/api/v2/config/svn" - } + }, + autocomplete: [ + { + href: "http://localhost:8081/scm/api/v2/autocomplete/users", + name: "users" + }, + { + href: "http://localhost:8081/scm/api/v2/autocomplete/groups", + name: "groups" + } + ] } }; -describe("fetch index resource", () => { - const index_url = "/api/v2/"; - const mockStore = configureMockStore([thunk]); +describe("index resource", () => { + describe("fetch index resource", () => { + const index_url = "/api/v2/"; + const mockStore = configureMockStore([thunk]); - afterEach(() => { - fetchMock.reset(); - fetchMock.restore(); - }); + afterEach(() => { + fetchMock.reset(); + fetchMock.restore(); + }); - it("should successfully fetch index resources when unauthenticated", () => { - fetchMock.getOnce(index_url, indexResourcesUnauthenticated); + it("should successfully fetch index resources when unauthenticated", () => { + fetchMock.getOnce(index_url, indexResourcesUnauthenticated); - const expectedActions = [ - { type: FETCH_INDEXRESOURCES_PENDING }, - { - type: FETCH_INDEXRESOURCES_SUCCESS, - payload: indexResourcesUnauthenticated - } - ]; + const expectedActions = [ + { type: FETCH_INDEXRESOURCES_PENDING }, + { + type: FETCH_INDEXRESOURCES_SUCCESS, + payload: indexResourcesUnauthenticated + } + ]; - const store = mockStore({}); - return store.dispatch(fetchIndexResources()).then(() => { - expect(store.getActions()).toEqual(expectedActions); + const store = mockStore({}); + return store.dispatch(fetchIndexResources()).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + it("should successfully fetch index resources when authenticated", () => { + fetchMock.getOnce(index_url, indexResourcesAuthenticated); + + const expectedActions = [ + { type: FETCH_INDEXRESOURCES_PENDING }, + { + type: FETCH_INDEXRESOURCES_SUCCESS, + payload: indexResourcesAuthenticated + } + ]; + + const store = mockStore({}); + return store.dispatch(fetchIndexResources()).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + it("should dispatch FETCH_INDEX_RESOURCES_FAILURE if request fails", () => { + fetchMock.getOnce(index_url, { + status: 500 + }); + + const store = mockStore({}); + return store.dispatch(fetchIndexResources()).then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(FETCH_INDEXRESOURCES_PENDING); + expect(actions[1].type).toEqual(FETCH_INDEXRESOURCES_FAILURE); + expect(actions[1].payload).toBeDefined(); + }); }); }); - it("should successfully fetch index resources when authenticated", () => { - fetchMock.getOnce(index_url, indexResourcesAuthenticated); + describe("index resources reducer", () => { + it("should return empty object, if state and action is undefined", () => { + expect(reducer()).toEqual({}); + }); - const expectedActions = [ - { type: FETCH_INDEXRESOURCES_PENDING }, - { - type: FETCH_INDEXRESOURCES_SUCCESS, - payload: indexResourcesAuthenticated - } - ]; + it("should return the same state, if the action is undefined", () => { + const state = { x: true }; + expect(reducer(state)).toBe(state); + }); - const store = mockStore({}); - return store.dispatch(fetchIndexResources()).then(() => { - expect(store.getActions()).toEqual(expectedActions); + it("should return the same state, if the action is unknown to the reducer", () => { + const state = { x: true }; + expect(reducer(state, { type: "EL_SPECIALE" })).toBe(state); + }); + + it("should store the index resources on FETCH_INDEXRESOURCES_SUCCESS", () => { + const newState = reducer( + {}, + fetchIndexResourcesSuccess(indexResourcesAuthenticated) + ); + expect(newState.links).toBe(indexResourcesAuthenticated._links); }); }); - it("should dispatch FETCH_INDEX_RESOURCES_FAILURE if request fails", () => { - fetchMock.getOnce(index_url, { - status: 500 + describe("index resources selectors", () => { + const error = new Error("something goes wrong"); + + it("should return true, when fetch index resources is pending", () => { + const state = { + pending: { + [FETCH_INDEXRESOURCES]: true + } + }; + expect(isFetchIndexResourcesPending(state)).toEqual(true); }); - const store = mockStore({}); - return store.dispatch(fetchIndexResources()).then(() => { - const actions = store.getActions(); - expect(actions[0].type).toEqual(FETCH_INDEXRESOURCES_PENDING); - expect(actions[1].type).toEqual(FETCH_INDEXRESOURCES_FAILURE); - expect(actions[1].payload).toBeDefined(); + it("should return false, when fetch index resources is not pending", () => { + expect(isFetchIndexResourcesPending({})).toEqual(false); + }); + + it("should return error when fetch index resources did fail", () => { + const state = { + failure: { + [FETCH_INDEXRESOURCES]: error + } + }; + expect(getFetchIndexResourcesFailure(state)).toEqual(error); + }); + + it("should return undefined when fetch index resources did not fail", () => { + expect(getFetchIndexResourcesFailure({})).toBe(undefined); + }); + + it("should return all links", () => { + const state = { + indexResources: { + links: indexResourcesAuthenticated._links + } + }; + expect(getLinks(state)).toBe(indexResourcesAuthenticated._links); + }); + + // ui plugins link + it("should return ui plugins link when authenticated and has permission to see it", () => { + const state = { + indexResources: { + links: indexResourcesAuthenticated._links + } + }; + expect(getUiPluginsLink(state)).toBe( + "http://localhost:8081/scm/api/v2/ui/plugins" + ); + }); + + it("should return ui plugins links when unauthenticated and has permission to see it", () => { + const state = { + indexResources: { + links: indexResourcesUnauthenticated._links + } + }; + expect(getUiPluginsLink(state)).toBe( + "http://localhost:8081/scm/api/v2/ui/plugins" + ); + }); + + // me link + it("should return me link when authenticated and has permission to see it", () => { + const state = { + indexResources: { + links: indexResourcesAuthenticated._links + } + }; + expect(getMeLink(state)).toBe("http://localhost:8081/scm/api/v2/me/"); + }); + + it("should return undefined for me link when unauthenticated or has not permission to see it", () => { + const state = { + indexResources: { + links: indexResourcesUnauthenticated._links + } + }; + expect(getMeLink(state)).toBe(undefined); + }); + + // logout link + it("should return logout link when authenticated and has permission to see it", () => { + const state = { + indexResources: { + links: indexResourcesAuthenticated._links + } + }; + expect(getLogoutLink(state)).toBe( + "http://localhost:8081/scm/api/v2/auth/access_token" + ); + }); + + it("should return undefined for logout link when unauthenticated or has not permission to see it", () => { + const state = { + indexResources: { + links: indexResourcesUnauthenticated._links + } + }; + expect(getLogoutLink(state)).toBe(undefined); + }); + + // login link + it("should return login link when unauthenticated and has permission to see it", () => { + const state = { + indexResources: { + links: indexResourcesUnauthenticated._links + } + }; + expect(getLoginLink(state)).toBe( + "http://localhost:8081/scm/api/v2/auth/access_token" + ); + }); + + it("should return undefined for login link when authenticated or has not permission to see it", () => { + const state = { + indexResources: { + links: indexResourcesAuthenticated._links + } + }; + expect(getLoginLink(state)).toBe(undefined); + }); + + // users link + it("should return users link when authenticated and has permission to see it", () => { + const state = { + indexResources: { + links: indexResourcesAuthenticated._links + } + }; + expect(getUsersLink(state)).toBe( + "http://localhost:8081/scm/api/v2/users/" + ); + }); + + it("should return undefined for users link when unauthenticated or has not permission to see it", () => { + const state = { + indexResources: { + links: indexResourcesUnauthenticated._links + } + }; + expect(getUsersLink(state)).toBe(undefined); + }); + + // groups link + it("should return groups link when authenticated and has permission to see it", () => { + const state = { + indexResources: { + links: indexResourcesAuthenticated._links + } + }; + expect(getGroupsLink(state)).toBe( + "http://localhost:8081/scm/api/v2/groups/" + ); + }); + + it("should return undefined for groups link when unauthenticated or has not permission to see it", () => { + const state = { + indexResources: { + links: indexResourcesUnauthenticated._links + } + }; + expect(getGroupsLink(state)).toBe(undefined); + }); + + // config link + it("should return config link when authenticated and has permission to see it", () => { + const state = { + indexResources: { + links: indexResourcesAuthenticated._links + } + }; + expect(getConfigLink(state)).toBe( + "http://localhost:8081/scm/api/v2/config" + ); + }); + + it("should return undefined for config link when unauthenticated or has not permission to see it", () => { + const state = { + indexResources: { + links: indexResourcesUnauthenticated._links + } + }; + expect(getConfigLink(state)).toBe(undefined); + }); + + // repositories link + it("should return repositories link when authenticated and has permission to see it", () => { + const state = { + indexResources: { + links: indexResourcesAuthenticated._links + } + }; + expect(getRepositoriesLink(state)).toBe( + "http://localhost:8081/scm/api/v2/repositories/" + ); + }); + + it("should return config for repositories link when unauthenticated or has not permission to see it", () => { + const state = { + indexResources: { + links: indexResourcesUnauthenticated._links + } + }; + expect(getRepositoriesLink(state)).toBe(undefined); + }); + + // hgConfig link + it("should return hgConfig link when authenticated and has permission to see it", () => { + const state = { + indexResources: { + links: indexResourcesAuthenticated._links + } + }; + expect(getHgConfigLink(state)).toBe( + "http://localhost:8081/scm/api/v2/config/hg" + ); + }); + + it("should return config for hgConfig link when unauthenticated or has not permission to see it", () => { + const state = { + indexResources: { + links: indexResourcesUnauthenticated._links + } + }; + expect(getHgConfigLink(state)).toBe(undefined); + }); + + // gitConfig link + it("should return gitConfig link when authenticated and has permission to see it", () => { + const state = { + indexResources: { + links: indexResourcesAuthenticated._links + } + }; + expect(getGitConfigLink(state)).toBe( + "http://localhost:8081/scm/api/v2/config/git" + ); + }); + + it("should return config for gitConfig link when unauthenticated or has not permission to see it", () => { + const state = { + indexResources: { + links: indexResourcesUnauthenticated._links + } + }; + expect(getGitConfigLink(state)).toBe(undefined); + }); + + // svnConfig link + it("should return svnConfig link when authenticated and has permission to see it", () => { + const state = { + indexResources: { + links: indexResourcesAuthenticated._links + } + }; + expect(getSvnConfigLink(state)).toBe( + "http://localhost:8081/scm/api/v2/config/svn" + ); + }); + + it("should return config for svnConfig link when unauthenticated or has not permission to see it", () => { + const state = { + indexResources: { + links: indexResourcesUnauthenticated._links + } + }; + expect(getSvnConfigLink(state)).toBe(undefined); + }); + + // Autocomplete links + it("should return link collection", () => { + const state = { + indexResources: { + links: indexResourcesAuthenticated._links + } + }; + expect(getLinkCollection(state, "autocomplete")).toEqual( + indexResourcesAuthenticated._links.autocomplete + ); + }); + + it("should return user autocomplete link", () => { + const state = { + indexResources: { + links: indexResourcesAuthenticated._links + } + }; + expect(getUserAutoCompleteLink(state)).toEqual( + "http://localhost:8081/scm/api/v2/autocomplete/users" + ); + }); + + it("should return group autocomplete link", () => { + const state = { + indexResources: { + links: indexResourcesAuthenticated._links + } + }; + expect(getGroupAutoCompleteLink(state)).toEqual( + "http://localhost:8081/scm/api/v2/autocomplete/groups" + ); }); }); }); - -describe("index resources reducer", () => { - it("should return empty object, if state and action is undefined", () => { - expect(reducer()).toEqual({}); - }); - - it("should return the same state, if the action is undefined", () => { - const state = { x: true }; - expect(reducer(state)).toBe(state); - }); - - it("should return the same state, if the action is unknown to the reducer", () => { - const state = { x: true }; - expect(reducer(state, { type: "EL_SPECIALE" })).toBe(state); - }); - - it("should store the index resources on FETCH_INDEXRESOURCES_SUCCESS", () => { - const newState = reducer( - {}, - fetchIndexResourcesSuccess(indexResourcesAuthenticated) - ); - expect(newState.links).toBe(indexResourcesAuthenticated._links); - }); -}); - -describe("index resources selectors", () => { - const error = new Error("something goes wrong"); - - it("should return true, when fetch index resources is pending", () => { - const state = { - pending: { - [FETCH_INDEXRESOURCES]: true - } - }; - expect(isFetchIndexResourcesPending(state)).toEqual(true); - }); - - it("should return false, when fetch index resources is not pending", () => { - expect(isFetchIndexResourcesPending({})).toEqual(false); - }); - - it("should return error when fetch index resources did fail", () => { - const state = { - failure: { - [FETCH_INDEXRESOURCES]: error - } - }; - expect(getFetchIndexResourcesFailure(state)).toEqual(error); - }); - - it("should return undefined when fetch index resources did not fail", () => { - expect(getFetchIndexResourcesFailure({})).toBe(undefined); - }); - - it("should return all links", () => { - const state = { - indexResources: { - links: indexResourcesAuthenticated._links - } - }; - expect(getLinks(state)).toBe(indexResourcesAuthenticated._links); - }); - - // ui plugins link - it("should return ui plugins link when authenticated and has permission to see it", () => { - const state = { - indexResources: { - links: indexResourcesAuthenticated._links - } - }; - expect(getUiPluginsLink(state)).toBe( - "http://localhost:8081/scm/api/v2/ui/plugins" - ); - }); - - it("should return ui plugins links when unauthenticated and has permission to see it", () => { - const state = { - indexResources: { - links: indexResourcesUnauthenticated._links - } - }; - expect(getUiPluginsLink(state)).toBe( - "http://localhost:8081/scm/api/v2/ui/plugins" - ); - }); - - // me link - it("should return me link when authenticated and has permission to see it", () => { - const state = { - indexResources: { - links: indexResourcesAuthenticated._links - } - }; - expect(getMeLink(state)).toBe("http://localhost:8081/scm/api/v2/me/"); - }); - - it("should return undefined for me link when unauthenticated or has not permission to see it", () => { - const state = { - indexResources: { - links: indexResourcesUnauthenticated._links - } - }; - expect(getMeLink(state)).toBe(undefined); - }); - - // logout link - it("should return logout link when authenticated and has permission to see it", () => { - const state = { - indexResources: { - links: indexResourcesAuthenticated._links - } - }; - expect(getLogoutLink(state)).toBe( - "http://localhost:8081/scm/api/v2/auth/access_token" - ); - }); - - it("should return undefined for logout link when unauthenticated or has not permission to see it", () => { - const state = { - indexResources: { - links: indexResourcesUnauthenticated._links - } - }; - expect(getLogoutLink(state)).toBe(undefined); - }); - - // login link - it("should return login link when unauthenticated and has permission to see it", () => { - const state = { - indexResources: { - links: indexResourcesUnauthenticated._links - } - }; - expect(getLoginLink(state)).toBe( - "http://localhost:8081/scm/api/v2/auth/access_token" - ); - }); - - it("should return undefined for login link when authenticated or has not permission to see it", () => { - const state = { - indexResources: { - links: indexResourcesAuthenticated._links - } - }; - expect(getLoginLink(state)).toBe(undefined); - }); - - // users link - it("should return users link when authenticated and has permission to see it", () => { - const state = { - indexResources: { - links: indexResourcesAuthenticated._links - } - }; - expect(getUsersLink(state)).toBe("http://localhost:8081/scm/api/v2/users/"); - }); - - it("should return undefined for users link when unauthenticated or has not permission to see it", () => { - const state = { - indexResources: { - links: indexResourcesUnauthenticated._links - } - }; - expect(getUsersLink(state)).toBe(undefined); - }); - - // groups link - it("should return groups link when authenticated and has permission to see it", () => { - const state = { - indexResources: { - links: indexResourcesAuthenticated._links - } - }; - expect(getGroupsLink(state)).toBe("http://localhost:8081/scm/api/v2/groups/"); - }); - - it("should return undefined for groups link when unauthenticated or has not permission to see it", () => { - const state = { - indexResources: { - links: indexResourcesUnauthenticated._links - } - }; - expect(getGroupsLink(state)).toBe(undefined); - }); - - // config link - it("should return config link when authenticated and has permission to see it", () => { - const state = { - indexResources: { - links: indexResourcesAuthenticated._links - } - }; - expect(getConfigLink(state)).toBe( - "http://localhost:8081/scm/api/v2/config" - ); - }); - - it("should return undefined for config link when unauthenticated or has not permission to see it", () => { - const state = { - indexResources: { - links: indexResourcesUnauthenticated._links - } - }; - expect(getConfigLink(state)).toBe(undefined); - }); - - // repositories link - it("should return repositories link when authenticated and has permission to see it", () => { - const state = { - indexResources: { - links: indexResourcesAuthenticated._links - } - }; - expect(getRepositoriesLink(state)).toBe( - "http://localhost:8081/scm/api/v2/repositories/" - ); - }); - - it("should return config for repositories link when unauthenticated or has not permission to see it", () => { - const state = { - indexResources: { - links: indexResourcesUnauthenticated._links - } - }; - expect(getRepositoriesLink(state)).toBe(undefined); - }); - - // hgConfig link - it("should return hgConfig link when authenticated and has permission to see it", () => { - const state = { - indexResources: { - links: indexResourcesAuthenticated._links - } - }; - expect(getHgConfigLink(state)).toBe( - "http://localhost:8081/scm/api/v2/config/hg" - ); - }); - - it("should return config for hgConfig link when unauthenticated or has not permission to see it", () => { - const state = { - indexResources: { - links: indexResourcesUnauthenticated._links - } - }; - expect(getHgConfigLink(state)).toBe(undefined); - }); - - // gitConfig link - it("should return gitConfig link when authenticated and has permission to see it", () => { - const state = { - indexResources: { - links: indexResourcesAuthenticated._links - } - }; - expect(getGitConfigLink(state)).toBe( - "http://localhost:8081/scm/api/v2/config/git" - ); - }); - - it("should return config for gitConfig link when unauthenticated or has not permission to see it", () => { - const state = { - indexResources: { - links: indexResourcesUnauthenticated._links - } - }; - expect(getGitConfigLink(state)).toBe(undefined); - }); - - // svnConfig link - it("should return svnConfig link when authenticated and has permission to see it", () => { - const state = { - indexResources: { - links: indexResourcesAuthenticated._links - } - }; - expect(getSvnConfigLink(state)).toBe( - "http://localhost:8081/scm/api/v2/config/svn" - ); - }); - - it("should return config for svnConfig link when unauthenticated or has not permission to see it", () => { - const state = { - indexResources: { - links: indexResourcesUnauthenticated._links - } - }; - expect(getSvnConfigLink(state)).toBe(undefined); - }); -}); diff --git a/scm-ui/src/repos/permissions/components/CreatePermissionForm.js b/scm-ui/src/repos/permissions/components/CreatePermissionForm.js index a674f6b41f..13e6ee38ba 100644 --- a/scm-ui/src/repos/permissions/components/CreatePermissionForm.js +++ b/scm-ui/src/repos/permissions/components/CreatePermissionForm.js @@ -1,23 +1,30 @@ // @flow import React from "react"; -import {translate} from "react-i18next"; -import {Checkbox, InputField, SubmitButton} from "@scm-manager/ui-components"; +import { translate } from "react-i18next"; +import { Autocomplete, SubmitButton } from "@scm-manager/ui-components"; import TypeSelector from "./TypeSelector"; -import type {PermissionCollection, PermissionCreateEntry} from "@scm-manager/ui-types"; +import type { + PermissionCollection, + PermissionCreateEntry, + SelectValue +} from "@scm-manager/ui-types"; import * as validator from "./permissionValidation"; type Props = { t: string => string, createPermission: (permission: PermissionCreateEntry) => void, loading: boolean, - currentPermissions: PermissionCollection + currentPermissions: PermissionCollection, + groupAutoCompleteLink: string, + userAutoCompleteLink: string }; type State = { name: string, type: string, groupPermission: boolean, - valid: boolean + valid: boolean, + value?: SelectValue }; class CreatePermissionForm extends React.Component { @@ -28,13 +35,95 @@ class CreatePermissionForm extends React.Component { name: "", type: "READ", groupPermission: false, - valid: true + valid: true, + value: undefined }; } + permissionScopeChanged = event => { + const groupPermission = event.target.value === "GROUP_PERMISSION"; + this.setState({ + groupPermission: groupPermission, + valid: validator.isPermissionValid( + this.state.name, + groupPermission, + this.props.currentPermissions + ) + }); + this.setState({ ...this.state, groupPermission }); + }; + + loadUserAutocompletion = (inputValue: string) => { + return this.loadAutocompletion(this.props.userAutoCompleteLink, inputValue); + }; + + loadGroupAutocompletion = (inputValue: string) => { + return this.loadAutocompletion( + this.props.groupAutoCompleteLink, + inputValue + ); + }; + + loadAutocompletion(url: string, inputValue: string) { + const link = url + "?q="; + return fetch(link + inputValue) + .then(response => response.json()) + .then(json => { + return json.map(element => { + const label = element.displayName + ? `${element.displayName} (${element.id})` + : element.id; + return { + value: element, + label + }; + }); + }); + } + renderAutocompletionField = () => { + const { t } = this.props; + if (this.state.groupPermission) { + return ( + + ); + } + return ( + + ); + }; + + groupOrUserSelected = (value: SelectValue) => { + this.setState({ + value, + name: value.value.id, + valid: validator.isPermissionValid( + value.value.id, + this.state.groupPermission, + this.props.currentPermissions + ) + }); + }; + render() { const { t, loading } = this.props; - const { name, type, groupPermission } = this.state; + + const { type } = this.state; return (
@@ -42,20 +131,30 @@ class CreatePermissionForm extends React.Component { {t("permission.add-permission.add-permission-heading")}
- - +
+ + +
+ {this.renderAutocompletionField()} + { type: type }); }; - - handleNameChange = (name: string) => { - this.setState({ - name: name, - valid: validator.isPermissionValid( - name, - this.state.groupPermission, - this.props.currentPermissions - ) - }); - }; - handleGroupPermissionChange = (groupPermission: boolean) => { - this.setState({ - groupPermission: groupPermission, - valid: validator.isPermissionValid( - this.state.name, - groupPermission, - this.props.currentPermissions - ) - }); - }; } export default translate("repos")(CreatePermissionForm); diff --git a/scm-ui/src/repos/permissions/containers/Permissions.js b/scm-ui/src/repos/permissions/containers/Permissions.js index ee9ac281a5..1a8d6b9f3a 100644 --- a/scm-ui/src/repos/permissions/containers/Permissions.js +++ b/scm-ui/src/repos/permissions/containers/Permissions.js @@ -27,6 +27,10 @@ import SinglePermission from "./SinglePermission"; import CreatePermissionForm from "../components/CreatePermissionForm"; import type { History } from "history"; import { getPermissionsLink } from "../../modules/repos"; +import { + getGroupAutoCompleteLink, + getUserAutoCompleteLink +} from "../../../modules/indexResource"; type Props = { namespace: string, @@ -37,6 +41,8 @@ type Props = { hasPermissionToCreate: boolean, loadingCreatePermission: boolean, permissionsLink: string, + groupAutoCompleteLink: string, + userAutoCompleteLink: string, //dispatch functions fetchPermissions: (link: string, namespace: string, repoName: string) => void, @@ -92,7 +98,9 @@ class Permissions extends React.Component { namespace, repoName, loadingCreatePermission, - hasPermissionToCreate + hasPermissionToCreate, + userAutoCompleteLink, + groupAutoCompleteLink } = this.props; if (error) { return ( @@ -113,6 +121,8 @@ class Permissions extends React.Component { createPermission={permission => this.createPermission(permission)} loading={loadingCreatePermission} currentPermissions={permissions} + userAutoCompleteLink={userAutoCompleteLink} + groupAutoCompleteLink={groupAutoCompleteLink} /> ) : null; @@ -165,6 +175,8 @@ const mapStateToProps = (state, ownProps) => { ); const hasPermissionToCreate = hasCreatePermission(state, namespace, repoName); const permissionsLink = getPermissionsLink(state, namespace, repoName); + const groupAutoCompleteLink = getGroupAutoCompleteLink(state); + const userAutoCompleteLink = getUserAutoCompleteLink(state); return { namespace, repoName, @@ -173,7 +185,9 @@ const mapStateToProps = (state, ownProps) => { permissions, hasPermissionToCreate, loadingCreatePermission, - permissionsLink + permissionsLink, + groupAutoCompleteLink, + userAutoCompleteLink }; }; @@ -189,7 +203,9 @@ const mapDispatchToProps = dispatch => { repoName: string, callback?: () => void ) => { - dispatch(createPermission(link, permission, namespace, repoName, callback)); + dispatch( + createPermission(link, permission, namespace, repoName, callback) + ); }, createPermissionReset: (namespace: string, repoName: string) => { dispatch(createPermissionReset(namespace, repoName)); diff --git a/scm-ui/src/users/components/SetUserPassword.js b/scm-ui/src/users/components/SetUserPassword.js index 6c2c1ca25d..d318025f21 100644 --- a/scm-ui/src/users/components/SetUserPassword.js +++ b/scm-ui/src/users/components/SetUserPassword.js @@ -19,7 +19,8 @@ type State = { password: string, loading: boolean, error?: Error, - passwordChanged: boolean + passwordChanged: boolean, + passwordValid: boolean }; class SetUserPassword extends React.Component { @@ -32,7 +33,8 @@ class SetUserPassword extends React.Component { passwordConfirmationError: false, validatePasswordError: false, validatePassword: "", - passwordChanged: false + passwordChanged: false, + passwordValid: false }; } @@ -104,7 +106,7 @@ class SetUserPassword extends React.Component { key={this.state.passwordChanged ? "changed" : "unchanged"} /> @@ -112,8 +114,8 @@ class SetUserPassword extends React.Component { ); } - passwordChanged = (password: string) => { - this.setState({ ...this.state, password }); + passwordChanged = (password: string, passwordValid: boolean) => { + this.setState({ ...this.state, password, passwordValid: (!!password && passwordValid) }); }; onClose = () => { diff --git a/scm-ui/src/users/components/UserForm.js b/scm-ui/src/users/components/UserForm.js index 2003d22c89..bb9b0cbf41 100644 --- a/scm-ui/src/users/components/UserForm.js +++ b/scm-ui/src/users/components/UserForm.js @@ -22,7 +22,8 @@ type State = { user: User, mailValidationError: boolean, nameValidationError: boolean, - displayNameValidationError: boolean + displayNameValidationError: boolean, + passwordValid: boolean }; class UserForm extends React.Component { @@ -41,7 +42,8 @@ class UserForm extends React.Component { }, mailValidationError: false, displayNameValidationError: false, - nameValidationError: false + nameValidationError: false, + passwordValid: false }; } @@ -61,7 +63,6 @@ class UserForm extends React.Component { isValid = () => { const user = this.state.user; - const passwordValid = this.props.user ? !this.isFalsy(user.password) : true; return !( this.state.nameValidationError || this.state.mailValidationError || @@ -69,7 +70,7 @@ class UserForm extends React.Component { this.isFalsy(user.name) || this.isFalsy(user.displayName) || this.isFalsy(user.mail) || - passwordValid + !this.state.passwordValid ); }; @@ -166,9 +167,10 @@ class UserForm extends React.Component { }); }; - handlePasswordChange = (password: string) => { + handlePasswordChange = (password: string, passwordValid: boolean) => { this.setState({ - user: { ...this.state.user, password } + user: { ...this.state.user, password }, + passwordValid: !this.isFalsy(password) && passwordValid }); }; diff --git a/scm-ui/yarn.lock b/scm-ui/yarn.lock index 2167ed28c9..6aeb0f7703 100644 --- a/scm-ui/yarn.lock +++ b/scm-ui/yarn.lock @@ -583,6 +583,12 @@ "@babel/plugin-transform-react-jsx-self" "^7.0.0" "@babel/plugin-transform-react-jsx-source" "^7.0.0" +"@babel/runtime@^7.1.2": + version "7.1.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.1.5.tgz#4170907641cf1f61508f563ece3725150cc6fe39" + dependencies: + regenerator-runtime "^0.12.0" + "@babel/template@^7.1.0", "@babel/template@^7.1.2": version "7.1.2" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.1.2.tgz#090484a574fef5a2d2d7726a674eceda5c5b5644" @@ -613,6 +619,46 @@ lodash "^4.17.10" to-fast-properties "^2.0.0" +"@emotion/babel-utils@^0.6.4": + version "0.6.10" + resolved "https://registry.yarnpkg.com/@emotion/babel-utils/-/babel-utils-0.6.10.tgz#83dbf3dfa933fae9fc566e54fbb45f14674c6ccc" + dependencies: + "@emotion/hash" "^0.6.6" + "@emotion/memoize" "^0.6.6" + "@emotion/serialize" "^0.9.1" + convert-source-map "^1.5.1" + find-root "^1.1.0" + source-map "^0.7.2" + +"@emotion/hash@^0.6.2", "@emotion/hash@^0.6.6": + version "0.6.6" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.6.6.tgz#62266c5f0eac6941fece302abad69f2ee7e25e44" + +"@emotion/memoize@^0.6.1", "@emotion/memoize@^0.6.6": + version "0.6.6" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.6.6.tgz#004b98298d04c7ca3b4f50ca2035d4f60d2eed1b" + +"@emotion/serialize@^0.9.1": + version "0.9.1" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.9.1.tgz#a494982a6920730dba6303eb018220a2b629c145" + dependencies: + "@emotion/hash" "^0.6.6" + "@emotion/memoize" "^0.6.6" + "@emotion/unitless" "^0.6.7" + "@emotion/utils" "^0.8.2" + +"@emotion/stylis@^0.7.0": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.7.1.tgz#50f63225e712d99e2b2b39c19c70fff023793ca5" + +"@emotion/unitless@^0.6.2", "@emotion/unitless@^0.6.7": + version "0.6.7" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.6.7.tgz#53e9f1892f725b194d5e6a1684a7b394df592397" + +"@emotion/utils@^0.8.2": + version "0.8.2" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.8.2.tgz#576ff7fb1230185b619a75d258cbc98f0867a8dc" + "@fortawesome/fontawesome-free@^5.3.1": version "5.3.1" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.3.1.tgz#5466b8f31c1f493a96754c1426c25796d0633dd9" @@ -652,9 +698,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.21": - version "0.0.21" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.21.tgz#f8b5fa355415cc67b8aaf8744e1701a299dff647" +"@scm-manager/ui-bundler@^0.0.22": + version "0.0.22" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.22.tgz#6eaed4e1f0b1fbc6ed1ebbf7eb0f5585f760949a" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" @@ -1144,6 +1190,23 @@ babel-messages@^6.23.0: dependencies: babel-runtime "^6.22.0" +babel-plugin-emotion@^9.2.11: + version "9.2.11" + resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-9.2.11.tgz#319c005a9ee1d15bb447f59fe504c35fd5807728" + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@emotion/babel-utils" "^0.6.4" + "@emotion/hash" "^0.6.2" + "@emotion/memoize" "^0.6.1" + "@emotion/stylis" "^0.7.0" + babel-plugin-macros "^2.0.0" + babel-plugin-syntax-jsx "^6.18.0" + convert-source-map "^1.5.0" + find-root "^1.1.0" + mkdirp "^0.5.1" + source-map "^0.5.7" + touch "^2.0.1" + babel-plugin-istanbul@^4.1.6: version "4.1.6" resolved "http://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz#36c59b2192efce81c5b378321b74175add1c9a45" @@ -1157,6 +1220,17 @@ babel-plugin-jest-hoist@^23.2.0: version "23.2.0" resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-23.2.0.tgz#e61fae05a1ca8801aadee57a6d66b8cefaf44167" +babel-plugin-macros@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.4.2.tgz#21b1a2e82e2130403c5ff785cba6548e9b644b28" + dependencies: + cosmiconfig "^5.0.5" + resolve "^1.8.1" + +babel-plugin-syntax-jsx@^6.18.0: + version "6.18.0" + resolved "http://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" + babel-plugin-syntax-object-rest-spread@^6.13.0: version "6.13.0" resolved "http://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" @@ -1667,12 +1741,24 @@ cached-path-relative@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.1.tgz#d09c4b52800aa4c078e2dd81a869aac90d2e54e7" +caller-callsite@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" + dependencies: + callsites "^2.0.0" + caller-path@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" dependencies: callsites "^0.2.0" +caller-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" + dependencies: + caller-callsite "^2.0.0" + callsite@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" @@ -2049,7 +2135,7 @@ contains-path@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" -convert-source-map@1.X, convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.5.1: +convert-source-map@1.X, convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.5.1: version "1.6.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" dependencies: @@ -2086,6 +2172,15 @@ core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" +cosmiconfig@^5.0.5: + version "5.0.7" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.0.7.tgz#39826b292ee0d78eda137dfa3173bd1c21a43b04" + dependencies: + import-fresh "^2.0.0" + is-directory "^0.3.1" + js-yaml "^3.9.0" + parse-json "^4.0.0" + coveralls@^2.11.3: version "2.13.3" resolved "https://registry.yarnpkg.com/coveralls/-/coveralls-2.13.3.tgz#9ad7c2ae527417f361e8b626483f48ee92dd2bc7" @@ -2103,6 +2198,18 @@ create-ecdh@^4.0.0: bn.js "^4.1.0" elliptic "^6.0.0" +create-emotion@^9.2.12: + version "9.2.12" + resolved "https://registry.yarnpkg.com/create-emotion/-/create-emotion-9.2.12.tgz#0fc8e7f92c4f8bb924b0fef6781f66b1d07cb26f" + dependencies: + "@emotion/hash" "^0.6.2" + "@emotion/memoize" "^0.6.1" + "@emotion/stylis" "^0.7.0" + "@emotion/unitless" "^0.6.2" + csstype "^2.5.2" + stylis "^3.5.0" + stylis-rule-sheet "^0.0.10" + create-hash@^1.1.0, create-hash@^1.1.2: version "1.2.0" resolved "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" @@ -2213,6 +2320,10 @@ cssstyle@^1.0.0: dependencies: cssom "0.3.x" +csstype@^2.5.2: + version "2.5.7" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.5.7.tgz#bf9235d5872141eccfb2d16d82993c6b149179ff" + currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" @@ -2476,6 +2587,12 @@ doctrine@^2.1.0: dependencies: esutils "^2.0.2" +dom-helpers@^3.3.1: + version "3.4.0" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8" + dependencies: + "@babel/runtime" "^7.1.2" + dom-serializer@0, dom-serializer@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" @@ -2584,6 +2701,13 @@ emoji-regex@^6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.5.1.tgz#9baea929b155565c11ea41c6626eaa65cef992c2" +emotion@^9.1.2: + version "9.2.12" + resolved "https://registry.yarnpkg.com/emotion/-/emotion-9.2.12.tgz#53925aaa005614e65c6e43db8243c843574d1ea9" + dependencies: + babel-plugin-emotion "^9.2.11" + create-emotion "^9.2.12" + encodeurl@~1.0.1, encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" @@ -3205,6 +3329,10 @@ find-node-modules@^1.0.4: findup-sync "0.4.2" merge "^1.2.0" +find-root@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" + find-up@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" @@ -4096,6 +4224,13 @@ immutable@^3: version "3.8.2" resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3" +import-fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" + dependencies: + caller-path "^2.0.0" + resolve-from "^3.0.0" + import-local@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/import-local/-/import-local-1.0.0.tgz#5e4ffdc03f4fe6c009c6729beb29631c2f8227bc" @@ -4297,6 +4432,10 @@ is-descriptor@^1.0.0, is-descriptor@^1.0.2: is-data-descriptor "^1.0.0" kind-of "^6.0.2" +is-directory@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" + is-dotfile@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" @@ -4956,7 +5095,7 @@ js-yaml@3.6.1: argparse "^1.0.7" esprima "^2.6.0" -js-yaml@^3.12.0, js-yaml@^3.7.0: +js-yaml@^3.12.0, js-yaml@^3.7.0, js-yaml@^3.9.0: version "3.12.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" dependencies: @@ -5431,7 +5570,7 @@ log-driver@1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/log-driver/-/log-driver-1.2.5.tgz#7ae4ec257302fd790d557cb10c97100d857b0056" -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" dependencies: @@ -5542,6 +5681,10 @@ mem@^1.1.0: dependencies: mimic-fn "^1.0.0" +memoize-one@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.0.3.tgz#cdfdd942853f1a1b4c71c5336b8c49da0bf0273c" + memoizee@0.4.X: version "0.4.14" resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.14.tgz#07a00f204699f9a95c2d9e77218271c7cd610d57" @@ -5959,7 +6102,7 @@ noms@0.0.0: inherits "^2.0.1" readable-stream "~1.0.31" -nopt@1.0.10: +nopt@1.0.10, nopt@~1.0.10: version "1.0.10" resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" dependencies: @@ -6805,6 +6948,12 @@ react-i18next@^7.9.0: html-parse-stringify2 "2.0.1" prop-types "^15.6.0" +react-input-autosize@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.1.tgz#ec428fa15b1592994fb5f9aa15bb1eb6baf420f8" + dependencies: + prop-types "^15.5.8" + react-is@^16.5.2: version "16.5.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.5.2.tgz#e2a7b7c3f5d48062eb769fcb123505eb928722e3" @@ -6819,6 +6968,10 @@ react-jss@^8.6.0: prop-types "^15.6.0" theming "^1.3.0" +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + react-redux@^5.0.7: version "5.0.7" resolved "http://registry.npmjs.org/react-redux/-/react-redux-5.0.7.tgz#0dc1076d9afb4670f993ffaef44b8f8c1155a4c8" @@ -6868,6 +7021,18 @@ react-router@^4.2.0, react-router@^4.3.1: prop-types "^15.6.1" warning "^4.0.1" +react-select@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/react-select/-/react-select-2.1.2.tgz#7a3e4c2b9efcd8c44ae7cf6ebb8b060ef69c513c" + dependencies: + classnames "^2.2.5" + emotion "^9.1.2" + memoize-one "^4.0.0" + prop-types "^15.6.0" + raf "^3.4.0" + react-input-autosize "^2.2.1" + react-transition-group "^2.2.1" + react-syntax-highlighter@^9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-9.0.1.tgz#cad91692e1976f68290f24762ac3451b1fec3d26" @@ -6887,6 +7052,15 @@ react-test-renderer@^16.0.0-0, react-test-renderer@^16.4.1: react-is "^16.5.2" schedule "^0.5.0" +react-transition-group@^2.2.1: + version "2.5.0" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.5.0.tgz#70bca0e3546102c4dc5cf3f5f57f73447cce6874" + dependencies: + dom-helpers "^3.3.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react-lifecycles-compat "^3.0.4" + react@^16.4.2: version "16.5.2" resolved "https://registry.yarnpkg.com/react/-/react-16.5.2.tgz#19f6b444ed139baa45609eee6dc3d318b3895d42" @@ -7068,6 +7242,10 @@ regenerator-runtime@^0.11.0, regenerator-runtime@^0.11.1: version "0.11.1" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" +regenerator-runtime@^0.12.0: + version "0.12.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" + regenerator-transform@^0.13.3: version "0.13.3" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.3.tgz#264bd9ff38a8ce24b06e0636496b2c856b57bcbb" @@ -7286,7 +7464,7 @@ resolve@1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" -resolve@^1.1.4, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.6.0: +resolve@^1.1.4, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.6.0, resolve@^1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26" dependencies: @@ -7683,6 +7861,10 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" +source-map@^0.7.2: + version "0.7.3" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" + space-separated-tokens@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.2.tgz#e95ab9d19ae841e200808cd96bc7bd0adbbb3412" @@ -7933,6 +8115,14 @@ strip-json-comments@^2.0.1, strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" +stylis-rule-sheet@^0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz#44e64a2b076643f4b52e5ff71efc04d8c3c4a430" + +stylis@^3.5.0: + version "3.5.4" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.4.tgz#f665f25f5e299cf3d64654ab949a57c768b73fbe" + subarg@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2" @@ -8135,6 +8325,12 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" +touch@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/touch/-/touch-2.0.2.tgz#ca0b2a3ae3211246a61b16ba9e6cbf1596287164" + dependencies: + nopt "~1.0.10" + tough-cookie@>=2.3.3, tough-cookie@^2.3.4, tough-cookie@~2.4.3: version "2.4.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" diff --git a/scm-webapp/src/main/java/sonia/scm/security/SecureKeyResolver.java b/scm-webapp/src/main/java/sonia/scm/security/SecureKeyResolver.java index a369db66bd..f1f29bfe51 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/SecureKeyResolver.java +++ b/scm-webapp/src/main/java/sonia/scm/security/SecureKeyResolver.java @@ -51,6 +51,7 @@ import static com.google.common.base.Preconditions.*; //~--- JDK imports ------------------------------------------------------------ import java.security.SecureRandom; +import java.util.Random; import javax.inject.Inject; import javax.inject.Singleton; @@ -88,12 +89,17 @@ public class SecureKeyResolver extends SigningKeyResolverAdapter */ @Inject @SuppressWarnings("unchecked") - public SecureKeyResolver(ConfigurationEntryStoreFactory storeFactory) + public SecureKeyResolver(ConfigurationEntryStoreFactory storeFactory) { + this(storeFactory, new SecureRandom()); + } + + SecureKeyResolver(ConfigurationEntryStoreFactory storeFactory, Random random) { store = storeFactory .withType(SecureKey.class) .withName(STORE_NAME) .build(); + this.random = random; } //~--- methods -------------------------------------------------------------- @@ -112,7 +118,9 @@ public class SecureKeyResolver extends SigningKeyResolverAdapter SecureKey key = store.get(subject); - checkState(key != null, "could not resolve key for subject %s", subject); + if (key == null) { + return getSecureKey(subject).getBytes(); + } return key.getBytes(); } @@ -161,7 +169,7 @@ public class SecureKeyResolver extends SigningKeyResolverAdapter //~--- fields --------------------------------------------------------------- /** secure randon */ - private final SecureRandom random = new SecureRandom(); + private final Random random; /** configuration entry store */ private final ConfigurationEntryStore store; diff --git a/scm-webapp/src/main/java/sonia/scm/web/security/TokenRefreshFilter.java b/scm-webapp/src/main/java/sonia/scm/web/security/TokenRefreshFilter.java index f85c0fbbbd..6747d40228 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/security/TokenRefreshFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/web/security/TokenRefreshFilter.java @@ -1,5 +1,6 @@ package sonia.scm.web.security; +import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -64,7 +65,13 @@ public class TokenRefreshFilter extends HttpFilter { } private void examineToken(HttpServletRequest request, HttpServletResponse response, BearerToken token) { - AccessToken accessToken = resolver.resolve(token); + AccessToken accessToken; + try { + accessToken = resolver.resolve(token); + } catch (AuthenticationException e) { + LOG.trace("could not resolve token", e); + return; + } if (accessToken instanceof JwtAccessToken) { refresher.refresh((JwtAccessToken) accessToken) .ifPresent(jwtAccessToken -> refreshToken(request, response, jwtAccessToken)); diff --git a/scm-webapp/src/test/java/sonia/scm/security/SecureKeyResolverTest.java b/scm-webapp/src/test/java/sonia/scm/security/SecureKeyResolverTest.java index c4f281537e..cce3fea2b1 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/SecureKeyResolverTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/SecureKeyResolverTest.java @@ -44,12 +44,16 @@ import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.store.ConfigurationEntryStore; import sonia.scm.store.ConfigurationEntryStoreFactory; +import java.util.Random; + import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.in; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.argThat; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -99,10 +103,11 @@ public class SecureKeyResolverTest * Method description * */ - @Test(expected = IllegalStateException.class) + @Test public void testResolveSigningKeyBytesWithoutKey() { - resolver.resolveSigningKeyBytes(null, Jwts.claims().setSubject("test")); + byte[] bytes = resolver.resolveSigningKeyBytes(null, Jwts.claims().setSubject("test")); + assertThat(bytes[0]).isEqualTo((byte) 42); } /** @@ -132,7 +137,9 @@ public class SecureKeyResolverTest assertThat(storeParameters.getType()).isEqualTo(SecureKey.class); return true; }))).thenReturn(store); - resolver = new SecureKeyResolver(factory); + Random random = mock(Random.class); + doAnswer(invocation -> ((byte[]) invocation.getArguments()[0])[0] = 42).when(random).nextBytes(any()); + resolver = new SecureKeyResolver(factory, random); } //~--- fields ---------------------------------------------------------------