adds combobox to select namespace strategy

This commit is contained in:
Sebastian Sdorra
2019-03-11 14:48:48 +01:00
parent 3f6d1ed4fd
commit 158bb8bf8a
9 changed files with 411 additions and 19 deletions

View File

@@ -0,0 +1,9 @@
// @flow
import type { Links } from "./hal";
export type NamespaceStrategies = {
current: string,
available: string[],
_links: Links
};

View File

@@ -26,3 +26,5 @@ export type { SubRepository, File } from "./Sources";
export type { SelectValue, AutocompleteObject } from "./Autocomplete";
export type { AvailableRepositoryPermissions, RepositoryRole } from "./AvailableRepositoryPermissions";
export type { NamespaceStrategies } from "./NamespaceStrategies";

View File

@@ -2,6 +2,7 @@
import React from "react";
import { translate } from "react-i18next";
import { SubmitButton, Notification } from "@scm-manager/ui-components";
import type { NamespaceStrategies } from "@scm-manager/ui-types";
import type { Config } from "@scm-manager/ui-types";
import ProxySettings from "./ProxySettings";
import GeneralSettings from "./GeneralSettings";
@@ -13,9 +14,11 @@ type Props = {
submitForm: Config => void,
config?: Config,
loading?: boolean,
t: string => string,
configReadPermission: boolean,
configUpdatePermission: boolean
configUpdatePermission: boolean,
namespaceStrategies?: NamespaceStrategies,
// context props
t: string => string,
};
type State = {
@@ -88,6 +91,7 @@ class ConfigForm extends React.Component<Props, State> {
const {
loading,
t,
namespaceStrategies,
configReadPermission,
configUpdatePermission
} = this.props;
@@ -118,6 +122,7 @@ class ConfigForm extends React.Component<Props, State> {
<form onSubmit={this.submit}>
{noPermissionNotification}
<GeneralSettings
namespaceStrategies={namespaceStrategies}
realmDescription={config.realmDescription}
enableRepositoryArchive={config.enableRepositoryArchive}
disableGroupingGrid={config.disableGroupingGrid}

View File

@@ -1,7 +1,9 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import { Checkbox, InputField } from "@scm-manager/ui-components";
import { Checkbox, InputField, Select } from "@scm-manager/ui-components";
import type { NamespaceStrategies } from "@scm-manager/ui-types";
import NamespaceStrategySelect from "./NamespaceStrategySelect";
type Props = {
realmDescription: string,
@@ -13,12 +15,15 @@ type Props = {
pluginUrl: string,
enabledXsrfProtection: boolean,
defaultNamespaceStrategy: string,
t: string => string,
namespaceStrategies?: NamespaceStrategies,
onChange: (boolean, any, string) => void,
hasUpdatePermission: boolean
hasUpdatePermission: boolean,
// context props
t: string => string
};
class GeneralSettings extends React.Component<Props> {
render() {
const {
t,
@@ -31,7 +36,8 @@ class GeneralSettings extends React.Component<Props> {
pluginUrl,
enabledXsrfProtection,
defaultNamespaceStrategy,
hasUpdatePermission
hasUpdatePermission,
namespaceStrategies
} = this.props;
return (
@@ -67,13 +73,14 @@ class GeneralSettings extends React.Component<Props> {
/>
</div>
<div className="column is-half">
<InputField
label={t("general-settings.default-namespace-strategy")}
onChange={this.handleDefaultNamespaceStrategyChange}
value={defaultNamespaceStrategy}
disabled={!hasUpdatePermission}
helpText={t("help.defaultNameSpaceStrategyHelpText")}
/>
<NamespaceStrategySelect
label={t("general-settings.default-namespace-strategy")}
onChange={this.handleDefaultNamespaceStrategyChange}
value={defaultNamespaceStrategy}
disabled={!hasUpdatePermission}
namespaceStrategies={namespaceStrategies}
helpText={t("help.defaultNameSpaceStrategyHelpText")}
/>
</div>
</div>
<div className="columns">

View File

@@ -0,0 +1,55 @@
//@flow
import React from "react";
import { translate, type TFunction } from "react-i18next";
import { Select } from "@scm-manager/ui-components";
import type { NamespaceStrategies } from "@scm-manager/ui-types";
type Props = {
namespaceStrategies: NamespaceStrategies,
label: string,
value?: string,
disabled?: boolean,
helpText?: string,
onChange: (value: string, name?: string) => void,
// context props
t: TFunction
};
class NamespaceStrategySelect extends React.Component<Props> {
createNamespaceOptions = () => {
const { namespaceStrategies, t } = this.props;
let available = [];
if (namespaceStrategies && namespaceStrategies.available) {
available = namespaceStrategies.available;
}
return available.map(ns => {
const key = "namespaceStrategies." + ns;
let label = t("namespaceStrategies." + ns);
if (label === key) {
label = ns;
}
return {
value: ns,
label: label
};
});
};
render() {
const { label, value, helpText, disabled, onChange } = this.props;
const nsOptions = this.createNamespaceOptions();
return (
<Select
label={label}
onChange={onChange}
value={value}
disabled={disabled}
options={nsOptions}
helpText={helpText}
/>
);
}
}
export default translate("plugins")(NamespaceStrategySelect);

View File

@@ -14,9 +14,15 @@ import {
modifyConfigReset
} from "../modules/config";
import { connect } from "react-redux";
import type { Config } from "@scm-manager/ui-types";
import type { Config, NamespaceStrategies } from "@scm-manager/ui-types";
import ConfigForm from "../components/form/ConfigForm";
import { getConfigLink } from "../../modules/indexResource";
import {
fetchNamespaceStrategiesIfNeeded,
getFetchNamespaceStrategiesFailure,
getNamespaceStrategies,
isFetchNamespaceStrategiesPending
} from "../modules/namespaceStrategies";
type Props = {
loading: boolean,
@@ -24,11 +30,13 @@ type Props = {
config: Config,
configUpdatePermission: boolean,
configLink: string,
namespaceStrategies?: NamespaceStrategies,
// dispatch functions
modifyConfig: (config: Config, callback?: () => void) => void,
fetchConfig: (link: string) => void,
configReset: void => void,
fetchNamespaceStrategiesIfNeeded: void => void,
// context objects
t: string => string
@@ -51,6 +59,7 @@ class GlobalConfig extends React.Component<Props, State> {
componentDidMount() {
this.props.configReset();
this.props.fetchNamespaceStrategiesIfNeeded();
if (this.props.configLink) {
this.props.fetchConfig(this.props.configLink);
} else {
@@ -103,7 +112,7 @@ class GlobalConfig extends React.Component<Props, State> {
};
renderContent = () => {
const { error, loading, config, configUpdatePermission } = this.props;
const { error, loading, config, configUpdatePermission, namespaceStrategies } = this.props;
const { configReadPermission } = this.state;
if (!error) {
return (
@@ -113,6 +122,7 @@ class GlobalConfig extends React.Component<Props, State> {
submitForm={config => this.modifyConfig(config)}
config={config}
loading={loading}
namespaceStrategies={namespaceStrategies}
configUpdatePermission={configUpdatePermission}
configReadPermission={configReadPermission}
/>
@@ -133,23 +143,33 @@ const mapDispatchToProps = dispatch => {
},
configReset: () => {
dispatch(modifyConfigReset());
},
fetchNamespaceStrategiesIfNeeded: () => {
dispatch(fetchNamespaceStrategiesIfNeeded());
}
};
};
const mapStateToProps = state => {
const loading = isFetchConfigPending(state) || isModifyConfigPending(state);
const error = getFetchConfigFailure(state) || getModifyConfigFailure(state);
const loading = isFetchConfigPending(state)
|| isModifyConfigPending(state)
|| isFetchNamespaceStrategiesPending(state);
const error = getFetchConfigFailure(state)
|| getModifyConfigFailure(state)
|| getFetchNamespaceStrategiesFailure(state);
const config = getConfig(state);
const configUpdatePermission = getConfigUpdatePermission(state);
const configLink = getConfigLink(state);
const namespaceStrategies = getNamespaceStrategies(state);
return {
loading,
error,
config,
configUpdatePermission,
configLink
configLink,
namespaceStrategies
};
};

View File

@@ -0,0 +1,105 @@
// @flow
import * as types from "../../modules/types";
import type { Action, NamespaceStrategies } from "@scm-manager/ui-types";
import { apiClient } from "@scm-manager/ui-components";
import { isPending } from "../../modules/pending";
import { getFailure } from "../../modules/failure";
export const FETCH_NAMESPACESTRATEGIES_TYPES =
"scm/config/FETCH_NAMESPACESTRATEGIES_TYPES";
export const FETCH_NAMESPACESTRATEGIES_TYPES_PENDING = `${FETCH_NAMESPACESTRATEGIES_TYPES}_${
types.PENDING_SUFFIX
}`;
export const FETCH_NAMESPACESTRATEGIES_TYPES_SUCCESS = `${FETCH_NAMESPACESTRATEGIES_TYPES}_${
types.SUCCESS_SUFFIX
}`;
export const FETCH_NAMESPACESTRATEGIES_TYPES_FAILURE = `${FETCH_NAMESPACESTRATEGIES_TYPES}_${
types.FAILURE_SUFFIX
}`;
export function fetchNamespaceStrategiesIfNeeded() {
return function(dispatch: any, getState: () => Object) {
const state = getState();
if (shouldFetchNamespaceStrategies(state)) {
return fetchNamespaceStrategies(dispatch, state.indexResources.links.namespaceStrategies.href);
}
};
}
function fetchNamespaceStrategies(dispatch: any, url: string) {
dispatch(fetchNamespaceStrategiesPending());
return apiClient
.get(url)
.then(response => response.json())
.then(namespaceStrategies => {
dispatch(fetchNamespaceStrategiesSuccess(namespaceStrategies));
})
.catch(error => {
dispatch(fetchNamespaceStrategiesFailure(error));
});
}
export function shouldFetchNamespaceStrategies(state: Object) {
if (
isFetchNamespaceStrategiesPending(state) ||
getFetchNamespaceStrategiesFailure(state)
) {
return false;
}
return !state.namespaceStrategies || !state.namespaceStrategies.current;
}
export function fetchNamespaceStrategiesPending(): Action {
return {
type: FETCH_NAMESPACESTRATEGIES_TYPES_PENDING
};
}
export function fetchNamespaceStrategiesSuccess(
namespaceStrategies: NamespaceStrategies
): Action {
return {
type: FETCH_NAMESPACESTRATEGIES_TYPES_SUCCESS,
payload: namespaceStrategies
};
}
export function fetchNamespaceStrategiesFailure(error: Error): Action {
return {
type: FETCH_NAMESPACESTRATEGIES_TYPES_FAILURE,
payload: error
};
}
// reducers
export default function reducer(
state: NamespaceStrategies = {},
action: Action = { type: "UNKNOWN" }
): NamespaceStrategies {
if (
action.type === FETCH_NAMESPACESTRATEGIES_TYPES_SUCCESS &&
action.payload
) {
return action.payload;
}
return state;
}
// selectors
export function getNamespaceStrategies(state: Object) {
if (state.namespaceStrategies) {
return state.namespaceStrategies;
}
return {};
}
export function isFetchNamespaceStrategiesPending(state: Object) {
return isPending(state, FETCH_NAMESPACESTRATEGIES_TYPES);
}
export function getFetchNamespaceStrategiesFailure(state: Object) {
return getFailure(state, FETCH_NAMESPACESTRATEGIES_TYPES);
}

View File

@@ -0,0 +1,187 @@
// @flow
import fetchMock from "fetch-mock";
import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk";
import {
FETCH_NAMESPACESTRATEGIES_TYPES,
FETCH_NAMESPACESTRATEGIES_TYPES_FAILURE,
FETCH_NAMESPACESTRATEGIES_TYPES_PENDING,
FETCH_NAMESPACESTRATEGIES_TYPES_SUCCESS,
fetchNamespaceStrategiesIfNeeded,
fetchNamespaceStrategiesSuccess,
shouldFetchNamespaceStrategies,
default as reducer,
getNamespaceStrategies,
isFetchNamespaceStrategiesPending,
getFetchNamespaceStrategiesFailure
} from "./namespaceStrategies";
const strategies = {
current: "sonia.scm.repository.UsernameNamespaceStrategy",
available: [
"sonia.scm.repository.UsernameNamespaceStrategy",
"sonia.scm.repository.CustomNamespaceStrategy",
"sonia.scm.repository.CurrentYearNamespaceStrategy",
"sonia.scm.repository.RepositoryTypeNamespaceStrategy"
],
_links: {
self: {
href: "http://localhost:8081/scm/api/v2/namespaceStrategies"
}
}
};
describe("namespace strategy caching", () => {
it("should fetch strategies, on empty state", () => {
expect(shouldFetchNamespaceStrategies({})).toBe(true);
});
it("should fetch strategies, on empty namespaceStrategies node", () => {
const state = {
namespaceStrategies: {}
};
expect(shouldFetchNamespaceStrategies(state)).toBe(true);
});
it("should not fetch strategies, on pending state", () => {
const state = {
pending: {
[FETCH_NAMESPACESTRATEGIES_TYPES]: true
}
};
expect(shouldFetchNamespaceStrategies(state)).toBe(false);
});
it("should not fetch strategies, on failure state", () => {
const state = {
failure: {
[FETCH_NAMESPACESTRATEGIES_TYPES]: new Error("no...")
}
};
expect(shouldFetchNamespaceStrategies(state)).toBe(false);
});
it("should not fetch strategies, if they are already fetched", () => {
const state = {
namespaceStrategies: {
current: "some"
}
};
expect(shouldFetchNamespaceStrategies(state)).toBe(false);
});
});
describe("namespace strategies fetch", () => {
const URL = "http://scm.hitchhiker.com/api/v2/namespaceStrategies";
const mockStore = configureMockStore([thunk]);
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
});
const createStore = (initialState = {}) => {
return mockStore({
...initialState,
indexResources: {
links: {
namespaceStrategies: {
href: URL
}
}
}
});
};
it("should successfully fetch strategies", () => {
fetchMock.getOnce(URL, strategies);
const expectedActions = [
{ type: FETCH_NAMESPACESTRATEGIES_TYPES_PENDING },
{
type: FETCH_NAMESPACESTRATEGIES_TYPES_SUCCESS,
payload: strategies
}
];
const store = createStore();
return store.dispatch(fetchNamespaceStrategiesIfNeeded()).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
it("should dispatch FETCH_NAMESPACESTRATEGIES_TYPES_FAILURE on server error", () => {
fetchMock.getOnce(URL, {
status: 500
});
const store = createStore();
return store.dispatch(fetchNamespaceStrategiesIfNeeded()).then(() => {
const actions = store.getActions();
expect(actions[0].type).toBe(FETCH_NAMESPACESTRATEGIES_TYPES_PENDING);
expect(actions[1].type).toBe(FETCH_NAMESPACESTRATEGIES_TYPES_FAILURE);
expect(actions[1].payload).toBeDefined();
});
});
it("should not dispatch any action, if the strategies are already fetched", () => {
const store = createStore({
namespaceStrategies: strategies
});
store.dispatch(fetchNamespaceStrategiesIfNeeded());
expect(store.getActions().length).toBe(0);
});
});
describe("namespace strategies reducer", () => {
it("should return unmodified state on unknown action", () => {
const state = [];
expect(reducer(state)).toBe(state);
});
it("should store the strategies on success", () => {
const newState = reducer([], fetchNamespaceStrategiesSuccess(strategies));
expect(newState).toBe(strategies);
});
});
describe("namespace strategy selectors", () => {
const error = new Error("The end of the universe");
it("should return an empty object", () => {
expect(getNamespaceStrategies({})).toEqual({});
});
it("should return the namespace strategies", () => {
const state = {
namespaceStrategies: strategies
};
expect(getNamespaceStrategies(state)).toBe(strategies);
});
it("should return true, when fetch namespace strategies is pending", () => {
const state = {
pending: {
[FETCH_NAMESPACESTRATEGIES_TYPES]: true
}
};
expect(isFetchNamespaceStrategiesPending(state)).toEqual(true);
});
it("should return false, when fetch strategies is not pending", () => {
expect(isFetchNamespaceStrategiesPending({})).toEqual(false);
});
it("should return error when fetch namespace strategies did fail", () => {
const state = {
failure: {
[FETCH_NAMESPACESTRATEGIES_TYPES]: error
}
};
expect(getFetchNamespaceStrategiesFailure(state)).toEqual(error);
});
it("should return undefined when fetch strategies did not fail", () => {
expect(getFetchNamespaceStrategiesFailure({})).toBe(undefined);
});
});

View File

@@ -15,6 +15,7 @@ import pending from "./modules/pending";
import failure from "./modules/failure";
import permissions from "./repos/permissions/modules/permissions";
import config from "./config/modules/config";
import namespaceStrategies from "./config/modules/namespaceStrategies";
import indexResources from "./modules/indexResource";
import type { BrowserHistory } from "history/createBrowserHistory";
@@ -38,7 +39,8 @@ function createReduxStore(history: BrowserHistory) {
groups,
auth,
config,
sources
sources,
namespaceStrategies
});
return createStore(