mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-07 22:15:45 +01:00
adds combobox to select namespace strategy
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
// @flow
|
||||
|
||||
import type { Links } from "./hal";
|
||||
|
||||
export type NamespaceStrategies = {
|
||||
current: string,
|
||||
available: string[],
|
||||
_links: Links
|
||||
};
|
||||
@@ -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";
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,11 +73,12 @@ class GeneralSettings extends React.Component<Props> {
|
||||
/>
|
||||
</div>
|
||||
<div className="column is-half">
|
||||
<InputField
|
||||
<NamespaceStrategySelect
|
||||
label={t("general-settings.default-namespace-strategy")}
|
||||
onChange={this.handleDefaultNamespaceStrategyChange}
|
||||
value={defaultNamespaceStrategy}
|
||||
disabled={!hasUpdatePermission}
|
||||
namespaceStrategies={namespaceStrategies}
|
||||
helpText={t("help.defaultNameSpaceStrategyHelpText")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
55
scm-ui/src/config/components/form/NamespaceStrategySelect.js
Normal file
55
scm-ui/src/config/components/form/NamespaceStrategySelect.js
Normal 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);
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
105
scm-ui/src/config/modules/namespaceStrategies.js
Normal file
105
scm-ui/src/config/modules/namespaceStrategies.js
Normal 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);
|
||||
}
|
||||
187
scm-ui/src/config/modules/namespaceStrategies.test.js
Normal file
187
scm-ui/src/config/modules/namespaceStrategies.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user