Fix es lint errors and warnings and enforce es lint as build breaker. (#1878)

Co-authored-by: Konstantin Schaper <konstantin.schaper@cloudogu.com>
This commit is contained in:
Eduard Heimbuch
2021-12-09 09:12:02 +01:00
committed by GitHub
parent 65d1e4ffd2
commit 289175331f
155 changed files with 905 additions and 791 deletions

View File

@@ -0,0 +1,2 @@
- type: changed
description: Enforce eslint to ensure accessible html ([#1878](https://github.com/scm-manager/scm-manager/pull/1878))

View File

@@ -11,6 +11,7 @@
"test": "lerna run --scope '@scm-manager/ui-*' test",
"e2e-tests": "lerna run --scope '@scm-manager/e2e-tests' ci",
"typecheck": "lerna run --scope '@scm-manager/ui-*' typecheck",
"lint": "lerna run --scope '@scm-manager/ui-*' lint",
"serve": "ui-scripts serve development",
"deploy": "ui-scripts publish",
"set-version": "ui-scripts version"

View File

@@ -55,6 +55,26 @@ task typecheck(type: YarnTask) {
}
}
task lint(type: YarnTask) {
args = ['lint']
inputs.files(fileTree(project.projectDir) {
include 'ui-*/src/**'
include 'ui-*/**/*.js'
include 'ui-*/**/*.jsx'
include 'ui-*/**/*.ts'
include 'ui-*/**/*.tsx'
})
.withPathSensitivity(PathSensitivity.RELATIVE)
outputs.file('build/tmp/lint/marker')
dependsOn('yarn_install')
doLast {
File directory = new File(project.buildDir, 'tmp/lint')
directory.mkdirs()
File marker = new File(directory, 'marker')
marker.createNewFile()
}
}
task test(type: YarnTask) {
args = ['run', 'test']
inputs.files(fileTree(project.projectDir) {
@@ -103,7 +123,7 @@ task updateUiTestTimestamps(type: TouchFiles) {
}
task check {
dependsOn('typecheck', 'test', 'chromatic', 'checkLicenses')
dependsOn('typecheck', 'test', 'chromatic', 'checkLicenses', 'lint')
}
yarn_install {

View File

@@ -30,12 +30,12 @@ import { requiredLink } from "./links";
export const useImportLog = (logId: string): ApiResult<string> => {
const link = useRequiredIndexLink("importLog").replace("{logId}", logId);
return useQuery<string, Error>(["importLog", logId], () => apiClient.get(link).then((response) => response.text()));
return useQuery<string, Error>(["importLog", logId], () => apiClient.get(link).then(response => response.text()));
};
export const useImportRepositoryFromUrl = (repositoryType: RepositoryType) => {
const url = requiredLink(repositoryType, "import", "url");
const { isLoading, error, data, mutate } = useMutation<Repository, Error, RepositoryUrlImport>((repo) =>
const { isLoading, error, data, mutate } = useMutation<Repository, Error, RepositoryUrlImport>(repo =>
apiClient
.post(url, repo, "application/vnd.scmm-repository+json;v=2")
.then(fetchResourceFromLocationHeader)
@@ -46,14 +46,14 @@ export const useImportRepositoryFromUrl = (repositoryType: RepositoryType) => {
isLoading,
error,
importRepositoryFromUrl: (repository: RepositoryUrlImport) => mutate(repository),
importedRepository: data,
importedRepository: data
};
};
const importRepository = (url: string, repository: RepositoryCreation, file: File, password?: string) => {
return apiClient
.postBinary(url, (formData) => {
formData.append("bundle", file, file?.name);
.postBinary(url, formData => {
formData.append("bundle", file, file.name);
formData.append("repository", JSON.stringify({ ...repository, password }));
})
.then(fetchResourceFromLocationHeader)
@@ -82,9 +82,9 @@ export const useImportRepositoryFromBundle = (repositoryType: RepositoryType) =>
repository,
file,
compressed,
password,
password
}),
importedRepository: data,
importedRepository: data
};
};
@@ -107,8 +107,8 @@ export const useImportFullRepository = (repositoryType: RepositoryType) => {
mutate({
repository,
file,
password,
password
}),
importedRepository: data,
importedRepository: data
};
};

View File

@@ -37,7 +37,7 @@ import {
useRepository,
useRepositoryTypes,
useUnarchiveRepository,
useUpdateRepository,
useUpdateRepository
} from "./repositories";
import { Repository } from "@scm-manager/ui-types";
import { QueryClient } from "react-query";
@@ -50,25 +50,25 @@ describe("Test repository hooks", () => {
type: "git",
_links: {
delete: {
href: "/r/spaceships/heartOfGold",
href: "/r/spaceships/heartOfGold"
},
update: {
href: "/r/spaceships/heartOfGold",
href: "/r/spaceships/heartOfGold"
},
archive: {
href: "/r/spaceships/heartOfGold/archive",
href: "/r/spaceships/heartOfGold/archive"
},
unarchive: {
href: "/r/spaceships/heartOfGold/unarchive",
},
},
href: "/r/spaceships/heartOfGold/unarchive"
}
}
};
const repositoryCollection = {
_embedded: {
repositories: [heartOfGold],
repositories: [heartOfGold]
},
_links: {},
_links: {}
};
afterEach(() => {
@@ -78,7 +78,7 @@ describe("Test repository hooks", () => {
describe("useRepositories tests", () => {
const expectCollection = async (queryClient: QueryClient, request?: UseRepositoriesRequest) => {
const { result, waitFor } = renderHook(() => useRepositories(request), {
wrapper: createWrapper(undefined, queryClient),
wrapper: createWrapper(undefined, queryClient)
});
await waitFor(() => {
return !!result.current.data;
@@ -91,8 +91,8 @@ describe("Test repository hooks", () => {
setIndexLink(queryClient, "repositories", "/repos");
fetchMock.get("/api/v2/repos", repositoryCollection, {
query: {
sortBy: "namespaceAndName",
},
sortBy: "namespaceAndName"
}
});
await expectCollection(queryClient);
@@ -104,12 +104,12 @@ describe("Test repository hooks", () => {
fetchMock.get("/api/v2/repos", repositoryCollection, {
query: {
sortBy: "namespaceAndName",
page: "42",
},
page: "42"
}
});
await expectCollection(queryClient, {
page: 42,
page: 42
});
});
@@ -118,8 +118,8 @@ describe("Test repository hooks", () => {
setIndexLink(queryClient, "repositories", "/repos");
fetchMock.get("/api/v2/spaceships", repositoryCollection, {
query: {
sortBy: "namespaceAndName",
},
sortBy: "namespaceAndName"
}
});
await expectCollection(queryClient, {
@@ -127,10 +127,10 @@ describe("Test repository hooks", () => {
namespace: "spaceships",
_links: {
repositories: {
href: "/spaceships",
},
},
},
href: "/spaceships"
}
}
}
});
});
@@ -140,12 +140,12 @@ describe("Test repository hooks", () => {
fetchMock.get("/api/v2/repos", repositoryCollection, {
query: {
sortBy: "namespaceAndName",
q: "heart",
},
q: "heart"
}
});
await expectCollection(queryClient, {
search: "heart",
search: "heart"
});
});
@@ -154,8 +154,8 @@ describe("Test repository hooks", () => {
setIndexLink(queryClient, "repositories", "/repos");
fetchMock.get("/api/v2/repos", repositoryCollection, {
query: {
sortBy: "namespaceAndName",
},
sortBy: "namespaceAndName"
}
});
await expectCollection(queryClient);
@@ -168,7 +168,7 @@ describe("Test repository hooks", () => {
const queryClient = createInfiniteCachingClient();
setIndexLink(queryClient, "repositories", "/repos");
const { result } = renderHook(() => useRepositories({ disabled: true }), {
wrapper: createWrapper(undefined, queryClient),
wrapper: createWrapper(undefined, queryClient)
});
expect(result.current.isLoading).toBe(false);
@@ -185,19 +185,18 @@ describe("Test repository hooks", () => {
fetchMock.postOnce("/api/v2/r", {
status: 201,
headers: {
Location: "/r/spaceships/heartOfGold",
},
Location: "/r/spaceships/heartOfGold"
}
});
fetchMock.getOnce("/api/v2/r/spaceships/heartOfGold", heartOfGold);
const { result, waitForNextUpdate } = renderHook(() => useCreateRepository(), {
wrapper: createWrapper(undefined, queryClient),
wrapper: createWrapper(undefined, queryClient)
});
const repository = {
...heartOfGold,
contextEntries: [],
...heartOfGold
};
await act(() => {
@@ -216,19 +215,18 @@ describe("Test repository hooks", () => {
fetchMock.postOnce("/api/v2/r?initialize=true", {
status: 201,
headers: {
Location: "/r/spaceships/heartOfGold",
},
Location: "/r/spaceships/heartOfGold"
}
});
fetchMock.getOnce("/api/v2/r/spaceships/heartOfGold", heartOfGold);
const { result, waitForNextUpdate } = renderHook(() => useCreateRepository(), {
wrapper: createWrapper(undefined, queryClient),
wrapper: createWrapper(undefined, queryClient)
});
const repository = {
...heartOfGold,
contextEntries: [],
...heartOfGold
};
await act(() => {
@@ -245,16 +243,15 @@ describe("Test repository hooks", () => {
setIndexLink(queryClient, "repositories", "/r");
fetchMock.postOnce("/api/v2/r", {
status: 201,
status: 201
});
const { result, waitForNextUpdate } = renderHook(() => useCreateRepository(), {
wrapper: createWrapper(undefined, queryClient),
wrapper: createWrapper(undefined, queryClient)
});
const repository = {
...heartOfGold,
contextEntries: [],
...heartOfGold
};
await act(() => {
@@ -274,7 +271,7 @@ describe("Test repository hooks", () => {
fetchMock.get("/api/v2/r/spaceships/heartOfGold", heartOfGold);
const { result, waitFor } = renderHook(() => useRepository("spaceships", "heartOfGold"), {
wrapper: createWrapper(undefined, queryClient),
wrapper: createWrapper(undefined, queryClient)
});
await waitFor(() => {
return !!result.current.data;
@@ -293,15 +290,15 @@ describe("Test repository hooks", () => {
{
name: "git",
displayName: "Git",
_links: {},
},
],
_links: {}
}
]
},
_links: {},
_links: {}
});
const { result, waitFor } = renderHook(() => useRepositoryTypes(), {
wrapper: createWrapper(undefined, queryClient),
wrapper: createWrapper(undefined, queryClient)
});
await waitFor(() => {
return !!result.current.data;
@@ -322,11 +319,11 @@ describe("Test repository hooks", () => {
const deleteRepository = async (options?: UseDeleteRepositoryOptions) => {
fetchMock.deleteOnce("/api/v2/r/spaceships/heartOfGold", {
status: 204,
status: 204
});
const { result, waitForNextUpdate } = renderHook(() => useDeleteRepository(options), {
wrapper: createWrapper(undefined, queryClient),
wrapper: createWrapper(undefined, queryClient)
});
await act(() => {
@@ -371,9 +368,9 @@ describe("Test repository hooks", () => {
it("should call onSuccess callback", async () => {
let repo;
await deleteRepository({
onSuccess: (repository) => {
onSuccess: repository => {
repo = repository;
},
}
});
expect(repo).toEqual(heartOfGold);
});
@@ -388,11 +385,11 @@ describe("Test repository hooks", () => {
const updateRepository = async () => {
fetchMock.putOnce("/api/v2/r/spaceships/heartOfGold", {
status: 204,
status: 204
});
const { result, waitForNextUpdate } = renderHook(() => useUpdateRepository(), {
wrapper: createWrapper(undefined, queryClient),
wrapper: createWrapper(undefined, queryClient)
});
await act(() => {
@@ -436,11 +433,11 @@ describe("Test repository hooks", () => {
const archiveRepository = async () => {
fetchMock.postOnce("/api/v2/r/spaceships/heartOfGold/archive", {
status: 204,
status: 204
});
const { result, waitForNextUpdate } = renderHook(() => useArchiveRepository(), {
wrapper: createWrapper(undefined, queryClient),
wrapper: createWrapper(undefined, queryClient)
});
await act(() => {
@@ -484,11 +481,11 @@ describe("Test repository hooks", () => {
const unarchiveRepository = async () => {
fetchMock.postOnce("/api/v2/r/spaceships/heartOfGold/unarchive", {
status: 204,
status: 204
});
const { result, waitForNextUpdate } = renderHook(() => useUnarchiveRepository(), {
wrapper: createWrapper(undefined, queryClient),
wrapper: createWrapper(undefined, queryClient)
});
await act(() => {

View File

@@ -113,7 +113,7 @@ const ErrorDisplay: FC<ErrorDisplayProps> = ({ error, errorInfo, fallback: Fallb
const fallbackProps = {
error,
errorInfo,
errorInfo
};
return <FallbackComponent {...fallbackProps} />;
@@ -135,7 +135,7 @@ class ErrorBoundary extends React.Component<Props, State> {
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
this.setState({
error,
errorInfo,
errorInfo
});
}

View File

@@ -33,7 +33,7 @@ type BaseProps = {
label?: string;
name?: string;
placeholder?: string;
value?: string;
value?: string | number;
type?: string;
autofocus?: boolean;
onReturnPressed?: () => void;
@@ -44,7 +44,7 @@ type BaseProps = {
helpText?: string;
className?: string;
testId?: string;
defaultValue?: string;
defaultValue?: string | number;
readOnly?: boolean;
};

View File

@@ -22,8 +22,10 @@
* SOFTWARE.
*/
import { RouteProps } from "react-router-dom";
export type RoutingProps = {
to: string;
activeOnlyWhenExact?: boolean;
activeWhenMatch?: (route: any) => boolean;
activeWhenMatch?: (route: RouteProps) => boolean;
};

View File

@@ -53,8 +53,8 @@ const withRoute = (route: string) => {
};
storiesOf("Secondary Navigation", module)
.addDecorator((story) => <StateMenuContextProvider>{story()}</StateMenuContextProvider>)
.addDecorator((story) => (
.addDecorator(story => <StateMenuContextProvider>{story()}</StateMenuContextProvider>)
.addDecorator(story => (
<Columns className="columns">
<div className="column is-3">{story()}</div>
</Columns>
@@ -92,7 +92,7 @@ storiesOf("Secondary Navigation", module)
<SecondaryNavigation label="Hitchhiker">
<SecondaryNavigationItem to="/42" icon="fas fa-puzzle-piece" label="Puzzle 42" title="Puzzle 42" />
<SecondaryNavigationItem
activeWhenMatch={(route) => route.location.pathname === "/hog"}
activeWhenMatch={route => route.location?.pathname === "/hog"}
to="/heart-of-gold"
icon="fas fa-star"
label="Heart Of Gold"

View File

@@ -22,7 +22,7 @@
* SOFTWARE.
*/
jest.mock("react-i18next", () => ({
export const jestMock = jest.mock("react-i18next", () => ({
// this mock makes sure any components using the translate HoC receive the t function as a prop
withTranslation: () => (Component: any) => {
Component.defaultProps = {
@@ -32,8 +32,6 @@ jest.mock("react-i18next", () => ({
return Component;
},
useTranslation: (ns: string) => {
return [
(key: string) => key
];
return [(key: string) => key];
}
}));

27
scm-ui/ui-tests/index.ts Normal file
View File

@@ -0,0 +1,27 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
export * from "./enzyme";
export * from "./enzyme-router";
export * from "./i18n";

View File

@@ -0,0 +1,32 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { Config } from "./Config";
import { HalRepresentation } from "./hal";
export type ConfigChangeHandler = <Name extends Exclude<keyof Config, keyof HalRepresentation>>(
isValid: boolean,
changedValue: Config[Name],
name: Name
) => void;

View File

@@ -22,7 +22,7 @@
* SOFTWARE.
*/
import { PagedCollection, Links, HalRepresentation } from "./hal";
import { HalRepresentation, Links, PagedCollection } from "./hal";
export type NamespaceAndName = {
namespace: string;
@@ -53,7 +53,7 @@ export type Repository = HalRepresentation &
};
export type RepositoryCreation = RepositoryBase & {
contextEntries: { [key: string]: any };
contextEntries?: { [key: string]: object | undefined };
};
export type RepositoryUrlImport = RepositoryCreation & {

View File

@@ -71,3 +71,4 @@ export * from "./ApiKeys";
export * from "./PublicKeys";
export * from "./GlobalPermissions";
export * from "./Search";
export * from "./General";

View File

@@ -25,7 +25,8 @@
"systemjs": "0.21.6"
},
"scripts": {
"test": "jest"
"test": "jest",
"lint": "eslint src"
},
"devDependencies": {
"@scm-manager/jest-preset": "^2.13.0",

View File

@@ -65,16 +65,16 @@ const reducer = (state: State = initialState, action: ActionTypes = { type: ACTI
...state,
indexResources: {
version: action.payload.version,
links: action.payload._links,
},
links: action.payload._links
}
};
}
case "scm/me_success": {
return {
...state,
auth: {
me: action.payload,
},
me: action.payload
}
};
}
default: {
@@ -90,14 +90,14 @@ const store = createStore(reducer, initialState);
export const fetchIndexResourcesSuccess = (index: IndexResources): ActionTypes => {
return {
type: ACTION_TYPE_INDEX,
payload: index,
payload: index
};
};
export const fetchMeSuccess = (me: Me): ActionTypes => {
return {
type: ACTION_TYPE_ME,
payload: me,
payload: me
};
};

View File

@@ -40,9 +40,10 @@ const mapDispatchToProps = (dispatch: Dispatch<ActionTypes>) => {
},
onMeFetched: (me: Me) => {
dispatch(fetchMeSuccess(me));
},
}
};
};
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore no clue how to type it
export default connect(undefined, mapDispatchToProps)(ReduxAwareApiProvider);

View File

@@ -24,11 +24,12 @@
import React from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import { Checkbox, InputField, Subtitle } from "@scm-manager/ui-components";
import { ConfigChangeHandler } from "@scm-manager/ui-types";
type Props = WithTranslation & {
baseUrl: string;
forceBaseUrl: boolean;
onChange: (p1: boolean, p2: any, p3: string) => void;
onChange: ConfigChangeHandler;
hasUpdatePermission: boolean;
};

View File

@@ -23,7 +23,7 @@
*/
import React, { FC, useState, useEffect, FormEvent } from "react";
import { useTranslation } from "react-i18next";
import { Config, NamespaceStrategies } from "@scm-manager/ui-types";
import { Config, ConfigChangeHandler, NamespaceStrategies } from "@scm-manager/ui-types";
import { Level, Notification, SubmitButton } from "@scm-manager/ui-components";
import ProxySettings from "./ProxySettings";
import GeneralSettings from "./GeneralSettings";
@@ -45,7 +45,7 @@ const ConfigForm: FC<Props> = ({
loading,
configReadPermission,
configUpdatePermission,
namespaceStrategies,
namespaceStrategies
}) => {
const [t] = useTranslation("config");
const [innerConfig, setInnerConfig] = useState<Config>({
@@ -74,7 +74,7 @@ const ConfigForm: FC<Props> = ({
mailDomainName: "",
emergencyContacts: [],
enabledApiKeys: true,
_links: {},
_links: {}
});
const [showNotification, setShowNotification] = useState(false);
const [changed, setChanged] = useState(false);
@@ -83,7 +83,7 @@ const ConfigForm: FC<Props> = ({
loginAttemptLimit: boolean;
}>({
loginAttemptLimitTimeout: false,
loginAttemptLimit: false,
loginAttemptLimit: false
});
useEffect(() => {
@@ -101,7 +101,7 @@ const ConfigForm: FC<Props> = ({
submitForm(innerConfig);
};
const onChange = (isValid: boolean, changedValue: any, name: string) => {
const onChange: ConfigChangeHandler = (isValid: boolean, changedValue: unknown, name: string) => {
setInnerConfig({ ...innerConfig, [name]: changedValue });
setError({ ...error, [name]: !isValid });
setChanged(true);
@@ -150,21 +150,21 @@ const ConfigForm: FC<Props> = ({
enabledApiKeys={innerConfig.enabledApiKeys}
emergencyContacts={innerConfig.emergencyContacts}
namespaceStrategy={innerConfig.namespaceStrategy}
onChange={(isValid, changedValue, name) => onChange(isValid, changedValue, name)}
onChange={onChange}
hasUpdatePermission={configUpdatePermission}
/>
<hr />
<LoginAttempt
loginAttemptLimit={innerConfig.loginAttemptLimit}
loginAttemptLimitTimeout={innerConfig.loginAttemptLimitTimeout}
onChange={(isValid, changedValue, name) => onChange(isValid, changedValue, name)}
onChange={onChange}
hasUpdatePermission={configUpdatePermission}
/>
<hr />
<BaseUrlSettings
baseUrl={innerConfig.baseUrl}
forceBaseUrl={innerConfig.forceBaseUrl}
onChange={(isValid, changedValue, name) => onChange(isValid, changedValue, name)}
onChange={onChange}
hasUpdatePermission={configUpdatePermission}
/>
<hr />
@@ -175,7 +175,7 @@ const ConfigForm: FC<Props> = ({
proxyUser={innerConfig.proxyUser ? innerConfig.proxyUser : ""}
enableProxy={innerConfig.enableProxy}
proxyExcludes={innerConfig.proxyExcludes}
onChange={(isValid, changedValue, name) => onChange(isValid, changedValue, name)}
onChange={onChange}
hasUpdatePermission={configUpdatePermission}
/>
<hr />

View File

@@ -24,13 +24,13 @@
import React, { FC } from "react";
import { useTranslation } from "react-i18next";
import { useUserSuggestions } from "@scm-manager/ui-api";
import { NamespaceStrategies, AnonymousMode, SelectValue } from "@scm-manager/ui-types";
import { NamespaceStrategies, AnonymousMode, SelectValue, ConfigChangeHandler } from "@scm-manager/ui-types";
import {
Checkbox,
InputField,
MemberNameTagGroup,
AutocompleteAddEntryToTableField,
Select,
Select
} from "@scm-manager/ui-components";
import NamespaceStrategySelect from "./NamespaceStrategySelect";
@@ -50,7 +50,7 @@ type Props = {
emergencyContacts: string[];
namespaceStrategy: string;
namespaceStrategies?: NamespaceStrategies;
onChange: (p1: boolean, p2: any, p3: string) => void;
onChange: ConfigChangeHandler;
hasUpdatePermission: boolean;
};
@@ -68,7 +68,7 @@ const GeneralSettings: FC<Props> = ({
namespaceStrategy,
namespaceStrategies,
onChange,
hasUpdatePermission,
hasUpdatePermission
}) => {
const { t } = useTranslation("config");
const userSuggestions = useUserSuggestions();
@@ -86,7 +86,7 @@ const GeneralSettings: FC<Props> = ({
onChange(true, value, "enabledUserConverter");
};
const handleAnonymousMode = (value: string) => {
onChange(true, value, "anonymousMode");
onChange(true, value as AnonymousMode, "anonymousMode");
};
const handleNamespaceStrategyChange = (value: string) => {
onChange(true, value, "namespaceStrategy");
@@ -181,7 +181,7 @@ const GeneralSettings: FC<Props> = ({
options={[
{ label: t("general-settings.anonymousMode.full"), value: "FULL" },
{ label: t("general-settings.anonymousMode.protocolOnly"), value: "PROTOCOL_ONLY" },
{ label: t("general-settings.anonymousMode.off"), value: "OFF" },
{ label: t("general-settings.anonymousMode.off"), value: "OFF" }
]}
helpText={t("help.allowAnonymousAccessHelpText")}
testId={"anonymous-mode-select"}

View File

@@ -24,11 +24,12 @@
import React from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import { InputField, Subtitle, validation as validator } from "@scm-manager/ui-components";
import { ConfigChangeHandler } from "@scm-manager/ui-types";
type Props = WithTranslation & {
loginAttemptLimit: number;
loginAttemptLimitTimeout: number;
onChange: (p1: boolean, p2: any, p3: string) => void;
onChange: ConfigChangeHandler;
hasUpdatePermission: boolean;
};
@@ -43,7 +44,7 @@ class LoginAttempt extends React.Component<Props, State> {
this.state = {
loginAttemptLimitError: false,
loginAttemptLimitTimeoutError: false,
loginAttemptLimitTimeoutError: false
};
}
render() {
@@ -84,17 +85,17 @@ class LoginAttempt extends React.Component<Props, State> {
handleLoginAttemptLimitChange = (value: string) => {
this.setState({
...this.state,
loginAttemptLimitError: !validator.isNumberValid(value),
loginAttemptLimitError: !validator.isNumberValid(value)
});
this.props.onChange(validator.isNumberValid(value), value, "loginAttemptLimit");
this.props.onChange(validator.isNumberValid(value), Number(value), "loginAttemptLimit");
};
handleLoginAttemptLimitTimeoutChange = (value: string) => {
this.setState({
...this.state,
loginAttemptLimitTimeoutError: !validator.isNumberValid(value),
loginAttemptLimitTimeoutError: !validator.isNumberValid(value)
});
this.props.onChange(validator.isNumberValid(value), value, "loginAttemptLimitTimeout");
this.props.onChange(validator.isNumberValid(value), Number(value), "loginAttemptLimitTimeout");
};
}

View File

@@ -43,7 +43,7 @@ class NamespaceStrategySelect extends React.Component<Props> {
available = namespaceStrategies.available;
}
return available.map((ns) => {
return available.map(ns => {
const key = "namespaceStrategies." + ns;
let label = t(key);
if (label === key) {
@@ -51,7 +51,7 @@ class NamespaceStrategySelect extends React.Component<Props> {
}
return {
value: ns,
label: label,
label: label
};
});
};

View File

@@ -25,6 +25,7 @@ import React from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import { AddEntryToTableField, Checkbox, InputField, Subtitle } from "@scm-manager/ui-components";
import ProxyExcludesTable from "../table/ProxyExcludesTable";
import { ConfigChangeHandler } from "@scm-manager/ui-types";
type Props = WithTranslation & {
proxyPassword: string;
@@ -33,14 +34,22 @@ type Props = WithTranslation & {
proxyUser: string;
enableProxy: boolean;
proxyExcludes: string[];
onChange: (p1: boolean, p2: any, p3: string) => void;
onChange: ConfigChangeHandler;
hasUpdatePermission: boolean;
};
class ProxySettings extends React.Component<Props> {
render() {
const { t, proxyPassword, proxyPort, proxyServer, proxyUser, enableProxy, proxyExcludes, hasUpdatePermission } =
this.props;
const {
t,
proxyPassword,
proxyPort,
proxyServer,
proxyUser,
enableProxy,
proxyExcludes,
hasUpdatePermission
} = this.props;
return (
<div>
@@ -101,7 +110,7 @@ class ProxySettings extends React.Component<Props> {
<div className="column is-full">
<ProxyExcludesTable
proxyExcludes={proxyExcludes}
onChange={(isValid, changedValue, name) => this.props.onChange(isValid, changedValue, name)}
onChange={this.props.onChange}
disabled={!enableProxy || !hasUpdatePermission}
/>
<AddEntryToTableField
@@ -122,7 +131,7 @@ class ProxySettings extends React.Component<Props> {
this.props.onChange(true, value, "proxyPassword");
};
handleProxyPortChange = (value: string) => {
this.props.onChange(true, value, "proxyPort");
this.props.onChange(true, Number(value), "proxyPort");
};
handleProxyServerChange = (value: string) => {
this.props.onChange(true, value, "proxyServer");
@@ -130,7 +139,7 @@ class ProxySettings extends React.Component<Props> {
handleProxyUserChange = (value: string) => {
this.props.onChange(true, value, "proxyUser");
};
handleEnableProxyChange = (value: string) => {
handleEnableProxyChange = (value: boolean) => {
this.props.onChange(true, value, "enableProxy");
};

View File

@@ -42,7 +42,7 @@ class ArrayConfigTable extends React.Component<Props> {
<LabelWithHelpIcon label={label} helpText={helpText} />
<table className="table is-hoverable is-fullwidth">
<tbody>
{items.map((item) => {
{items.map(item => {
return (
<tr key={item}>
<td>{item}</td>
@@ -66,7 +66,7 @@ class ArrayConfigTable extends React.Component<Props> {
}
removeEntry = (item: string) => {
const newItems = this.props.items.filter((name) => name !== item);
const newItems = this.props.items.filter(name => name !== item);
this.props.onRemove(newItems, item);
};
}

View File

@@ -24,10 +24,11 @@
import React from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import ArrayConfigTable from "./ArrayConfigTable";
import { ConfigChangeHandler } from "@scm-manager/ui-types";
type Props = WithTranslation & {
proxyExcludes: string[];
onChange: (p1: boolean, p2: any, p3: string) => void;
onChange: ConfigChangeHandler;
disabled: boolean;
};

View File

@@ -23,7 +23,7 @@
*/
import React, { FC } from "react";
import { useTranslation } from "react-i18next";
import { Redirect, Route, Switch, useRouteMatch } from "react-router-dom";
import { Redirect, Route, RouteProps, Switch, useRouteMatch } from "react-router-dom";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
import {
CustomQueryFlexWrappedColumns,
@@ -34,7 +34,7 @@ import {
SecondaryNavigationColumn,
StateMenuContextProvider,
SubNavigation,
urls,
urls
} from "@scm-manager/ui-components";
import AdminDetails from "./AdminDetails";
import PluginsOverview from "../plugins/containers/PluginsOverview";
@@ -51,16 +51,16 @@ const Admin: FC = () => {
const availablePluginsLink = links.availablePlugins;
const installedPluginsLink = links.installedPlugins;
const matchesRoles = (route: any) => {
const matchesRoles = (route: RouteProps) => {
const url = urls.matchedUrlFromMatch(match);
const regex = new RegExp(`${url}/role/`);
return route.location.pathname.match(regex);
return !!route.location?.pathname.match(regex);
};
const url = urls.matchedUrlFromMatch(match);
const extensionProps = {
links,
url,
url
};
return (

View File

@@ -60,7 +60,7 @@ const AdminDetails: FC = () => {
<h3 className="has-text-weight-medium">{t("admin.info.newRelease.title")}</h3>
<p>
{t("admin.info.newRelease.description", {
version: updateInfo?.latestVersion,
version: updateInfo?.latestVersion
})}
</p>
<a className="button is-warning is-pulled-right" target="_blank" href={updateInfo?.link} rel="noreferrer">

View File

@@ -34,7 +34,7 @@ const GlobalConfig: FC = () => {
const {
data: namespaceStrategies,
error: namespaceStrategiesLoadingError,
isLoading: isLoadingNamespaceStrategies,
isLoading: isLoadingNamespaceStrategies
} = useNamespaceStrategies();
const [t] = useTranslation("config");
const error = configLoadingError || namespaceStrategiesLoadingError || updateError || undefined;

View File

@@ -44,7 +44,7 @@ const Section: FC<SectionProps> = ({ pendingPlugins, type, label }) => {
<>
<strong>{label}</strong>
<ul>
{plugins.map((plugin) => (
{plugins.map(plugin => (
<li key={plugin.name}>{plugin.name}</li>
))}
</ul>

View File

@@ -72,8 +72,8 @@ class PluginActionModal extends React.Component<Props> {
<strong>{t("plugins.modal.updateQueue")}</strong>
<ul>
{installedPlugins._embedded.plugins
.filter((plugin) => plugin._links && plugin._links.update)
.map((plugin) => (
.filter(plugin => plugin._links && plugin._links.update)
.map(plugin => (
<li key={plugin.name}>{plugin.name}</li>
))}
</ul>
@@ -91,7 +91,7 @@ class PluginActionModal extends React.Component<Props> {
<>
<strong>{t("plugins.modal.installQueue")}</strong>
<ul>
{pendingPlugins._embedded.new.map((plugin) => (
{pendingPlugins._embedded.new.map(plugin => (
<li key={plugin.name}>{plugin.name}</li>
))}
</ul>
@@ -109,7 +109,7 @@ class PluginActionModal extends React.Component<Props> {
<>
<strong>{t("plugins.modal.updateQueue")}</strong>
<ul>
{pendingPlugins._embedded.update.map((plugin) => (
{pendingPlugins._embedded.update.map(plugin => (
<li key={plugin.name}>{plugin.name}</li>
))}
</ul>
@@ -127,7 +127,7 @@ class PluginActionModal extends React.Component<Props> {
<>
<strong>{t("plugins.modal.uninstallQueue")}</strong>
<ul>
{pendingPlugins._embedded.uninstall.map((plugin) => (
{pendingPlugins._embedded.uninstall.map(plugin => (
<li key={plugin.name}>{plugin.name}</li>
))}
</ul>

View File

@@ -47,7 +47,7 @@ export default class PluginAvatar extends React.Component<Props> {
<ExtensionPoint
name="plugins.plugin-avatar"
props={{
plugin,
plugin
}}
>
<Image src={plugin.avatarUrl ? plugin.avatarUrl : "/images/blib.jpg"} alt="Logo" />

View File

@@ -40,8 +40,8 @@ const ActionbarWrapper = styled.div`
}
`;
const IconWrapperStyle = styled.span.attrs((props) => ({
className: "level-item mb-0 p-2 is-clickable",
const IconWrapperStyle = styled.span.attrs(props => ({
className: "level-item mb-0 p-2 is-clickable"
}))`
border: 1px solid #cdcdcd; // $dark-25
border-radius: 4px;
@@ -53,7 +53,7 @@ const IconWrapperStyle = styled.span.attrs((props) => ({
const IconWrapper: FC<{ action: () => void }> = ({ action, children }) => {
return (
<IconWrapperStyle onClick={action} onKeyDown={(e) => e.key === "Enter" && action()} tabIndex={0}>
<IconWrapperStyle onClick={action} onKeyDown={e => e.key === "Enter" && action()} tabIndex={0}>
{children}
</IconWrapperStyle>
);

View File

@@ -33,7 +33,7 @@ type Props = {
};
const PluginGroupEntry: FC<Props> = ({ openModal, group }) => {
const entries = group.plugins.map((plugin) => {
const entries = group.plugins.map(plugin => {
return <PluginEntry plugin={plugin} openModal={openModal} key={plugin.name} />;
});
return <CardColumnGroup name={group.name} elements={entries} />;

View File

@@ -36,7 +36,7 @@ const PluginList: FC<Props> = ({ plugins, openModal }) => {
const groups = groupByCategory(plugins);
return (
<div className="content is-plugin-page">
{groups.map((group) => {
{groups.map(group => {
return <PluginGroupEntry group={group} openModal={openModal} key={group.name} />;
})}
</div>

View File

@@ -41,10 +41,10 @@ type ParentWithPluginAction = {
pluginAction?: PluginAction;
};
const ListParent = styled.div.attrs((props) => ({
className: "field-label is-inline-flex mr-0 has-text-left",
const ListParent = styled.div.attrs(props => ({
className: "field-label is-inline-flex mr-0 has-text-left"
}))<ParentWithPluginAction>`
min-width: ${(props) => (props.pluginAction === PluginAction.INSTALL ? "5.5em" : "10em")};
min-width: ${props => (props.pluginAction === PluginAction.INSTALL ? "5.5em" : "10em")};
`;
const ListChild = styled.div`
@@ -236,7 +236,7 @@ const PluginModal: FC<Props> = ({ onClose, pluginAction, plugin }) => {
return (
<Modal
title={t(`plugins.modal.title.${pluginAction}`, {
name: plugin.displayName ? plugin.displayName : plugin.name,
name: plugin.displayName ? plugin.displayName : plugin.name
})}
closeFunction={onClose}
body={body}

View File

@@ -48,7 +48,7 @@ class InstallSuccessNotification extends React.Component<Props> {
return (
<Notification type="success">
{this.createMessageForPluginAction()}{" "}
<NoStyleButton onClick={(_) => window.location.reload(true)} className="has-text-info">
<NoStyleButton onClick={_ => window.location.reload(true)} className="has-text-info">
{t("plugins.modal.reload")}
</NoStyleButton>
</Notification>

View File

@@ -33,7 +33,7 @@ export default function groupByCategory(plugins: Plugin[]): PluginGroup[] {
if (!group) {
group = {
name: groupName,
plugins: [],
plugins: []
};
groups[groupName] = group;
}

View File

@@ -32,7 +32,7 @@ import {
Loading,
Notification,
Subtitle,
Title,
Title
} from "@scm-manager/ui-components";
import PluginsList from "../components/PluginList";
import PluginTopActions from "../components/PluginTopActions";
@@ -48,7 +48,7 @@ export enum PluginAction {
INSTALL = "install",
UPDATE = "update",
UNINSTALL = "uninstall",
CLOUDOGU = "cloudoguInstall",
CLOUDOGU = "cloudoguInstall"
}
export type PluginModalContent = {
@@ -65,12 +65,12 @@ const PluginsOverview: FC<Props> = ({ installed }) => {
const {
data: availablePlugins,
isLoading: isLoadingAvailablePlugins,
error: availablePluginsError,
error: availablePluginsError
} = useAvailablePlugins({ enabled: !installed });
const {
data: installedPlugins,
isLoading: isLoadingInstalledPlugins,
error: installedPluginsError,
error: installedPluginsError
} = useInstalledPlugins({ enabled: installed });
const { data: pendingPlugins, isLoading: isLoadingPendingPlugins, error: pendingPluginsError } = usePendingPlugins();
const [showPendingModal, setShowPendingModal] = useState(false);
@@ -167,7 +167,7 @@ const PluginsOverview: FC<Props> = ({ installed }) => {
const computeUpdateAllSize = () => {
const outdatedPlugins = collection?._embedded.plugins.filter((p: Plugin) => p._links.update).length;
return t("plugins.outdatedPlugins", {
count: outdatedPlugins,
count: outdatedPlugins
});
};

View File

@@ -58,7 +58,7 @@ class PermissionRoleDetails extends React.Component<Props> {
name="repositoryRole.role-details.information"
renderAll={true}
props={{
role,
role
}}
/>
</>

View File

@@ -67,12 +67,12 @@ const DeleteRepositoryRole: FC<Props> = ({ confirmDialog = true, role }: Props)
{
className: "is-outlined",
label: t("repositoryRole.delete.confirmAlert.submit"),
onClick: deleteRoleCallback,
onClick: deleteRoleCallback
},
{
label: t("repositoryRole.delete.confirmAlert.cancel"),
onClick: () => null,
},
onClick: () => null
}
]}
close={() => setShowConfirmAlert(false)}
/>

View File

@@ -42,8 +42,8 @@ const RepositoryRoleForm: FC<Props> = ({ role: initialRole, submitForm }) => {
name: "",
verbs: [],
_links: {
create: { href: createLink },
},
create: { href: createLink }
}
}
);
const availableVerbs = data?.verbs;
@@ -58,7 +58,7 @@ const RepositoryRoleForm: FC<Props> = ({ role: initialRole, submitForm }) => {
const handleVerbChange = (value: boolean, name: string) =>
setRole({
...role,
verbs: value ? [...role.verbs, name] : role.verbs.filter((v) => v !== name),
verbs: value ? [...role.verbs, name] : role.verbs.filter(v => v !== name)
});
const submit = (event: FormEvent<HTMLFormElement>) => {

View File

@@ -32,7 +32,7 @@ import {
Notification,
Subtitle,
Title,
urls,
urls
} from "@scm-manager/ui-components";
import PermissionRoleTable from "../components/PermissionRoleTable";
import { useRepositoryRoles } from "@scm-manager/ui-api";

View File

@@ -50,7 +50,7 @@ const SingleRepositoryRole: FC = () => {
const extensionProps = {
role,
url,
url
};
return (

View File

@@ -60,7 +60,7 @@ class LoginForm extends React.Component<Props, State> {
super(props);
this.state = {
username: "",
password: "",
password: ""
};
}
@@ -73,13 +73,13 @@ class LoginForm extends React.Component<Props, State> {
handleUsernameChange = (value: string) => {
this.setState({
username: value,
username: value
});
};
handlePasswordChange = (value: string) => {
this.setState({
password: value,
password: value
});
};

View File

@@ -76,23 +76,23 @@ class LoginInfo extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
loading: !!props.loginInfoLink,
loading: !!props.loginInfoLink
};
}
fetchLoginInfo = (url: string) => {
return fetch(url)
.then((response) => response.json())
.then((info) => {
.then(response => response.json())
.then(info => {
this.setState({
info,
loading: false,
loading: false
});
});
};
timeout = (ms: number, promise: Promise<any>) => {
return new Promise<LoginInfoResponse>((resolve, reject) => {
timeout = (ms: number, promise: Promise<void>) => {
return new Promise<void>((resolve, reject) => {
setTimeout(() => {
reject(new Error("timeout during fetch of login info"));
}, ms);
@@ -107,7 +107,7 @@ class LoginInfo extends React.Component<Props, State> {
}
this.timeout(1000, this.fetchLoginInfo(loginInfoLink)).catch(() => {
this.setState({
loading: false,
loading: false
});
});
}

View File

@@ -29,7 +29,7 @@ import {
Notification,
PasswordConfirmation,
SubmitButton,
Subtitle,
Subtitle
} from "@scm-manager/ui-components";
import { useTranslation } from "react-i18next";
import { Me } from "@scm-manager/ui-types";

View File

@@ -39,7 +39,7 @@ const Index: FC = () => {
// TODO check componentDidUpdate method for anonymous user stuff
i18next.on("languageChanged", (lng) => {
i18next.on("languageChanged", lng => {
document.documentElement.setAttribute("lang", lng);
});

View File

@@ -48,21 +48,15 @@ type AdminAccountCreation = {
passwordConfirmation: string;
};
const createAdmin = (link: string) => {
return (data: AdminAccountCreation) => {
return apiClient.post(link, data, "application/json").then(() => {
return new Promise<void>((resolve) => resolve());
});
};
};
const createAdmin = (link: string) => (data: AdminAccountCreation) => apiClient.post(link, data, "application/json");
const useCreateAdmin = (link: string) => {
const { mutate, isLoading, error, isSuccess } = useMutation<void, Error, AdminAccountCreation>(createAdmin(link));
const { mutate, isLoading, error, isSuccess } = useMutation<unknown, Error, AdminAccountCreation>(createAdmin(link));
return {
create: mutate,
isLoading,
error,
isCreated: isSuccess,
isCreated: isSuccess
};
};
@@ -74,16 +68,16 @@ const InitializationAdminAccountStep: FC<Props> = ({ data }) => {
displayName: "SCM Administrator",
email: "",
password: "",
passwordConfirmation: "",
passwordConfirmation: ""
},
mode: "onChange",
mode: "onChange"
});
const { create, isLoading, error, isCreated } = useCreateAdmin((data._links.initialAdminUser as Link).href);
useEffect(() => {
if (isCreated) {
window.location.reload(false);
window.location.reload();
}
}, [isCreated]);

View File

@@ -59,7 +59,7 @@ const LoginButton: FC<Props> = ({ burgerMode, links, className }) => {
from,
to,
className: headerButtonContentClassName,
content,
content
};
if (links?.login) {

View File

@@ -48,7 +48,7 @@ const LogoutButton: FC<Props> = ({ burgerMode, links, className }) => {
label,
className: headerButtonContentClassName,
content,
content
};
if (links?.logout) {

View File

@@ -56,7 +56,7 @@ type Props = {
authenticated?: boolean;
};
const Main: FC<Props> = (props) => {
const Main: FC<Props> = props => {
const { authenticated, me } = props;
const redirectUrlFactory = binder.getExtension("main.redirect", props);
let url = "/";

View File

@@ -30,7 +30,7 @@ import {
useClearNotifications,
useDismissNotification,
useNotifications,
useNotificationSubscription,
useNotificationSubscription
} from "@scm-manager/ui-api";
import { Notification, NotificationCollection } from "@scm-manager/ui-types";
import {
@@ -43,7 +43,7 @@ import {
ToastType,
Loading,
DateFromNow,
devices,
devices
} from "@scm-manager/ui-components";
const DropDownMenu = styled.div`
@@ -283,7 +283,7 @@ type NotificationCounterProps = {
const NotificationCounter = styled.span<NotificationCounterProps>`
position: absolute;
top: -0.5rem;
right: ${(props) => (props.count < 10 ? "0" : "-0.25")}rem;
right: ${props => (props.count < 10 ? "0" : "-0.25")}rem;
`;
type BellNotificationIconProps = {
@@ -351,14 +351,14 @@ const Notifications: FC<NotificationProps> = ({ className }) => {
"dropdown",
"is-hoverable",
{
"is-active": open,
"is-active": open
},
className
)}
onClick={(e) => e.stopPropagation()}
onClick={e => e.stopPropagation()}
>
<div className={classNames("is-flex", "dropdown-trigger", "is-clickable")}>
<BellNotificationIcon data={data} onClick={() => setOpen((o) => !o)} />
<BellNotificationIcon data={data} onClick={() => setOpen(o => !o)} />
</div>
<DropDownMenu className="dropdown-menu" id="dropdown-menu" role="menu">
<ErrorBox error={error} />

View File

@@ -328,7 +328,7 @@ const OmniSearch: FC = () => {
}
};
const { onKeyDown, index } = useKeyBoardNavigation(gotoDetailSearch, clearQuery, data?._embedded.hits);
const { onKeyDown, index } = useKeyBoardNavigation(gotoDetailSearch, clearQuery, data?._embedded?.hits);
return (
<div className={classNames("navbar-item", "field", "mb-0")}>
@@ -372,7 +372,7 @@ const OmniSearch: FC = () => {
gotoDetailSearch={gotoDetailSearch}
clear={clearQuery}
index={index}
hits={data._embedded.hits}
hits={data._embedded?.hits || []}
/>
) : null}
</DropdownMenu>

View File

@@ -53,7 +53,7 @@ class PluginLoader extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
message: "booting",
message: "booting"
};
}
@@ -61,26 +61,26 @@ class PluginLoader extends React.Component<Props, State> {
const { loaded } = this.props;
if (!loaded) {
this.setState({
message: "loading plugin information",
message: "loading plugin information"
});
this.getPlugins(this.props.link);
}
}
getPlugins = (link: string): Promise<any> => {
return apiClient
getPlugins = (link: string) => {
apiClient
.get(link)
.then((response) => response.text())
.then(response => response.text())
.then(JSON.parse)
.then((pluginCollection) => pluginCollection._embedded.plugins)
.then(pluginCollection => pluginCollection._embedded.plugins)
.then(this.loadPlugins)
.then(this.props.callback);
};
loadPlugins = (plugins: Plugin[]) => {
this.setState({
message: "loading plugins",
message: "loading plugins"
});
const promises = [];
@@ -89,21 +89,21 @@ class PluginLoader extends React.Component<Props, State> {
promises.push(this.loadPlugin(plugin));
}
return promises.reduce((chain, current) => {
return chain.then((chainResults) => {
return current.then((currentResult) => [...chainResults, currentResult]);
return chain.then(chainResults => {
return current.then(currentResult => [...chainResults, currentResult]);
});
}, Promise.resolve([]));
};
loadPlugin = (plugin: Plugin) => {
this.setState({
message: `loading ${plugin.name}`,
message: `loading ${plugin.name}`
});
const promises = [];
for (const bundle of plugin.bundles) {
promises.push(
loadBundle(bundle).catch((error) => this.setState({ error, errorMessage: `loading ${plugin.name} failed` }))
loadBundle(bundle).catch(error => this.setState({ error, errorMessage: `loading ${plugin.name} failed` }))
);
}
return Promise.all(promises);

View File

@@ -77,7 +77,7 @@ class ProfileInfo extends React.Component<Props> {
<th>{t("profile.groups")}</th>
<td className="p-0">
<ul>
{me.groups.map((group) => {
{me.groups.map(group => {
return <li>{group}</li>;
})}
</ul>

View File

@@ -22,24 +22,23 @@
* SOFTWARE.
*/
import React from "react";
import { withRouter } from "react-router-dom";
import { FC, ReactElement, useEffect, useRef } from "react";
type Props = {
location: any;
children: any;
location?: Location;
children: ReactElement;
};
class ScrollToTop extends React.Component<Props> {
componentDidUpdate(prevProps) {
if (this.props.location.pathname !== prevProps.location.pathname) {
const ScrollToTop: FC<Props> = ({ location, children }) => {
const previousLocation = useRef(location);
useEffect(() => {
if (previousLocation?.current?.pathname !== location?.pathname) {
window.scrollTo(0, 0);
}
}
}, [location]);
render() {
return this.props.children;
}
}
return children;
};
export default withRouter(ScrollToTop);
export default ScrollToTop;

View File

@@ -55,12 +55,12 @@ const BundleLoader = {
headers: {
Cache: "no-cache",
// identify the request as ajax request
"X-Requested-With": "XMLHttpRequest",
},
}).then((response) => {
"X-Requested-With": "XMLHttpRequest"
}
}).then(response => {
return response.text();
});
},
}
};
SystemJS.registry.set(BundleLoader.name, SystemJS.newModule(BundleLoader));
@@ -69,12 +69,13 @@ SystemJS.config({
baseURL: urls.withContextPath("/assets"),
meta: {
"/*": {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore typing missing, but seems required
esModule: true,
authorization: true,
loader: BundleLoader.name,
},
},
loader: BundleLoader.name
}
}
});
// We have to patch the resolve methods of SystemJS
@@ -89,17 +90,18 @@ const resolveModuleUrl = (key: string) => {
};
const defaultResolve = SystemJS.resolve;
SystemJS.resolve = function (key, parentName) {
SystemJS.resolve = function(key, parentName) {
const module = resolveModuleUrl(key);
return defaultResolve.apply(this, [module, parentName]);
};
const defaultResolveSync = SystemJS.resolveSync;
SystemJS.resolveSync = function (key, parentName) {
SystemJS.resolveSync = function(key, parentName) {
const module = resolveModuleUrl(key);
return defaultResolveSync.apply(this, [module, parentName]);
};
//eslint-disable-next-line @typescript-eslint/no-explicit-any
const expose = (name: string, cmp: any, defaultCmp?: any) => {
let mod = cmp;
if (defaultCmp) {
@@ -107,7 +109,7 @@ const expose = (name: string, cmp: any, defaultCmp?: any) => {
// https://github.com/systemjs/systemjs/issues/1749
mod = {
...cmp,
__useDefault: defaultCmp,
__useDefault: defaultCmp
};
}
SystemJS.set(name, SystemJS.newModule(mod));

View File

@@ -32,7 +32,7 @@ import {
MemberNameTagGroup,
SubmitButton,
Subtitle,
Textarea,
Textarea
} from "@scm-manager/ui-components";
import * as validator from "./groupValidation";
@@ -40,7 +40,7 @@ type Props = WithTranslation & {
submitForm: (p: Group) => void;
loading?: boolean;
group?: Group;
loadUserSuggestions: (p: string) => any;
loadUserSuggestions: (p: string) => Promise<SelectValue[]>;
};
type State = {
@@ -56,14 +56,14 @@ class GroupForm extends React.Component<Props, State> {
name: "",
description: "",
_embedded: {
members: [],
members: []
},
_links: {},
members: [],
type: "",
external: false,
external: false
},
nameValidationError: false,
nameValidationError: false
};
}
@@ -73,8 +73,8 @@ class GroupForm extends React.Component<Props, State> {
this.setState({
...this.state,
group: {
...group,
},
...group
}
});
}
}
@@ -183,13 +183,13 @@ class GroupForm extends React.Component<Props, State> {
);
}
memberListChanged = (membernames) => {
memberListChanged = membernames => {
this.setState({
...this.state,
group: {
...this.state.group,
members: membernames,
},
members: membernames
}
});
};
@@ -202,8 +202,8 @@ class GroupForm extends React.Component<Props, State> {
...this.state,
group: {
...this.state.group,
members: [...this.state.group.members, value.value.id],
},
members: [...this.state.group.members, value.value.id]
}
});
};
@@ -216,8 +216,8 @@ class GroupForm extends React.Component<Props, State> {
nameValidationError: !validator.isNameValid(name),
group: {
...this.state.group,
name,
},
name
}
});
};
@@ -225,8 +225,8 @@ class GroupForm extends React.Component<Props, State> {
this.setState({
group: {
...this.state.group,
description,
},
description
}
});
};
@@ -234,8 +234,8 @@ class GroupForm extends React.Component<Props, State> {
this.setState({
group: {
...this.state.group,
external,
},
external
}
});
};
}

View File

@@ -23,13 +23,12 @@
*/
import React from "react";
import { shallow } from "enzyme";
import "@scm-manager/ui-tests/enzyme";
import "@scm-manager/ui-tests/i18n";
import "@scm-manager/ui-tests";
import EditGroupNavLink from "./EditGroupNavLink";
it("should render nothing, if the edit link is missing", () => {
const group = {
_links: {},
_links: {}
};
const navLink = shallow(<EditGroupNavLink group={group} editUrl="/group/edit" />);
@@ -40,9 +39,9 @@ it("should render the navLink", () => {
const group = {
_links: {
update: {
href: "/groups",
},
},
href: "/groups"
}
}
};
const navLink = shallow(<EditGroupNavLink group={group} editUrl="/group/edit" />);

View File

@@ -46,7 +46,7 @@ export default class GroupMember extends React.Component<Props> {
);
}
showName(to: any, member: Member) {
showName(to: string, member: Member) {
if (member._links.self) {
return this.renderLink(to, member.name);
} else {

View File

@@ -62,12 +62,12 @@ export const DeleteGroup: FC<Props> = ({ confirmDialog = true, group }) => {
{
className: "is-outlined",
label: t("deleteGroup.confirmAlert.submit"),
onClick: deleteGroupCallback,
onClick: deleteGroupCallback
},
{
label: t("deleteGroup.confirmAlert.cancel"),
onClick: () => null,
},
onClick: () => null
}
]}
close={() => setShowConfirmAlert(false)}
/>

View File

@@ -31,7 +31,7 @@ import {
OverviewPageActions,
Page,
PageActions,
urls,
urls
} from "@scm-manager/ui-components";
import { GroupTable } from "./../components/table";
import { useGroups } from "@scm-manager/ui-api";

View File

@@ -36,7 +36,7 @@ import {
SecondaryNavigationColumn,
StateMenuContextProvider,
SubNavigation,
urls,
urls
} from "@scm-manager/ui-components";
import { Details } from "./../components/table";
import { EditGroupNavLink, SetPermissionsNavLink } from "./../components/navLinks";
@@ -62,7 +62,7 @@ const SingleGroup: FC = () => {
const extensionProps = {
group,
url,
url
};
return (

View File

@@ -23,6 +23,7 @@
*/
import i18n from "i18next";
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
import Backend from "i18next-fetch-backend";
import LanguageDetector from "i18next-browser-languagedetector";
@@ -48,19 +49,19 @@ i18n
debug: false,
interpolation: {
escapeValue: false, // not needed for react!!
escapeValue: false // not needed for react!!
},
react: {
wait: true,
useSuspense: false,
useSuspense: false
},
backend: {
loadPath: loadPath,
init: {
credentials: "same-origin",
},
credentials: "same-origin"
}
},
// configure LanguageDetector
@@ -69,8 +70,8 @@ i18n
// we only use browser configuration
order: ["navigator"],
// we do not cache the detected language
caches: [],
},
caches: []
}
});
export default i18n;

View File

@@ -55,7 +55,7 @@ export default class PermissionsWrapper extends React.Component<Props> {
return (
<div className="columns">
<StyledWrapper className={classNames("column", "is-half", "pb-0")}>
{permissionArray.slice(0, permissionArray.length / 2 + 1).map((p) => (
{permissionArray.slice(0, permissionArray.length / 2 + 1).map(p => (
<PermissionCheckbox
key={p}
name={p}
@@ -67,7 +67,7 @@ export default class PermissionsWrapper extends React.Component<Props> {
))}
</StyledWrapper>
<StyledWrapper className={classNames("column", "is-half")}>
{permissionArray.slice(permissionArray.length / 2 + 1, permissionArray.length).map((p) => (
{permissionArray.slice(permissionArray.length / 2 + 1, permissionArray.length).map(p => (
<PermissionCheckbox
key={p}
name={p}

View File

@@ -31,16 +31,14 @@ type Props = {
};
const SetGroupPermissions: FC<Props> = ({ group }) => {
const {
data: selectedPermissions,
isLoading: loadingPermissions,
error: permissionsLoadError,
} = useGroupPermissions(group);
const { data: selectedPermissions, isLoading: loadingPermissions, error: permissionsLoadError } = useGroupPermissions(
group
);
const {
isLoading: isUpdatingPermissions,
isUpdated: permissionsUpdated,
setPermissions,
error: permissionsUpdateError,
error: permissionsUpdateError
} = useSetGroupPermissions(group, selectedPermissions);
return (
<SetPermissions

View File

@@ -45,13 +45,13 @@ const SetPermissions: FC<Props> = ({
permissionsUpdateError,
updatePermissions,
permissionsUpdated,
selectedPermissions,
selectedPermissions
}) => {
const [t] = useTranslation("permissions");
const {
data: availablePermissions,
error: availablePermissionsLoadError,
isLoading: isLoadingAvailablePermissions,
isLoading: isLoadingAvailablePermissions
} = useAvailableGlobalPermissions();
const [permissions, setPermissions] = useState<Record<string, boolean>>({});
const [permissionsSubmitted, setPermissionsSubmitted] = useState(false);
@@ -61,8 +61,8 @@ const SetPermissions: FC<Props> = ({
useEffect(() => {
if (selectedPermissions && availablePermissions) {
const newPermissions: Record<string, boolean> = {};
availablePermissions.permissions.forEach((p) => (newPermissions[p] = false));
selectedPermissions.permissions.forEach((p) => (newPermissions[p] = true));
availablePermissions.permissions.forEach(p => (newPermissions[p] = false));
selectedPermissions.permissions.forEach(p => (newPermissions[p] = true));
setPermissions(newPermissions);
}
}, [availablePermissions, selectedPermissions]);
@@ -85,7 +85,7 @@ const SetPermissions: FC<Props> = ({
const valueChanged = (value: boolean, name: string) => {
setPermissions({
...permissions,
[name]: value,
[name]: value
});
setPermissionsChanged(true);
};
@@ -96,9 +96,11 @@ const SetPermissions: FC<Props> = ({
event.preventDefault();
if (permissions) {
const selectedPermissions = Object.entries(permissions)
.filter((e) => e[1])
.map((e) => e[0]);
updatePermissions!(selectedPermissions);
.filter(e => e[1])
.map(e => e[0]);
if (updatePermissions) {
updatePermissions(selectedPermissions);
}
}
};

View File

@@ -31,16 +31,14 @@ type Props = {
};
const SetUserPermissions: FC<Props> = ({ user }) => {
const {
data: selectedPermissions,
isLoading: loadingPermissions,
error: permissionsLoadError,
} = useUserPermissions(user);
const { data: selectedPermissions, isLoading: loadingPermissions, error: permissionsLoadError } = useUserPermissions(
user
);
const {
isLoading: isUpdatingPermissions,
isUpdated: permissionsUpdated,
setPermissions,
error: permissionsUpdateError,
error: permissionsUpdateError
} = useSetUserPermissions(user, selectedPermissions);
return (
<SetPermissions

View File

@@ -21,119 +21,84 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FormEvent } from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import { Branch, BranchCreation, Repository } from "@scm-manager/ui-types";
import React, { FC, FormEvent, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Branch, BranchCreation } from "@scm-manager/ui-types";
import { InputField, Level, Select, SubmitButton, validation as validator } from "@scm-manager/ui-components";
import { orderBranches } from "../util/orderBranches";
type Props = WithTranslation & {
type Props = {
submitForm: (p: BranchCreation) => void;
repository: Repository;
branches: Branch[];
loading?: boolean;
transmittedName?: string;
disabled?: boolean;
};
type State = {
source?: string;
name?: string;
nameValidationError: boolean;
};
const BranchForm: FC<Props> = ({ submitForm, branches, disabled, transmittedName, loading }) => {
const [t] = useTranslation("repos");
const [name, setName] = useState(transmittedName || "");
const [source, setSource] = useState("");
const [nameValid, setNameValid] = useState(false);
class BranchForm extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
useEffect(() => {
setNameValid(validator.isBranchValid(name));
}, [name]);
this.state = {
nameValidationError: false,
name: props.transmittedName,
};
}
const isValid = () => nameValid && source && name;
isFalsy(value?: string) {
return !value;
}
isValid = () => {
const { source, name } = this.state;
return !(this.state.nameValidationError || this.isFalsy(source) || this.isFalsy(name));
};
submit = (event: FormEvent<HTMLFormElement>) => {
const submit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (this.isValid()) {
this.props.submitForm({
name: this.state.name!,
parent: this.state.source!,
if (isValid()) {
submitForm({
name,
parent: source
});
}
};
render() {
const { t, branches, loading, transmittedName, disabled } = this.props;
const { name } = this.state;
orderBranches(branches);
const options = branches.map((branch) => ({
label: branch.name,
value: branch.name,
}));
orderBranches(branches);
const options = branches.map(branch => ({
label: branch.name,
value: branch.name
}));
return (
<>
<form onSubmit={this.submit}>
<div className="columns">
<div className="column">
<Select
name="source"
label={t("branches.create.source")}
options={options}
onChange={this.handleSourceChange}
loading={loading}
disabled={disabled}
/>
<InputField
name="name"
label={t("branches.create.name")}
onChange={this.handleNameChange}
value={name ? name : ""}
validationError={this.state.nameValidationError}
errorMessage={t("validation.branch.nameInvalid")}
disabled={!!transmittedName || disabled}
/>
</div>
return (
<>
<form onSubmit={submit}>
<div className="columns">
<div className="column">
<Select
name="source"
label={t("branches.create.source")}
options={options}
onChange={setSource}
loading={loading}
disabled={disabled}
/>
<InputField
name="name"
label={t("branches.create.name")}
onChange={setName}
value={name ? name : ""}
validationError={!nameValid}
errorMessage={t("validation.branch.nameInvalid")}
disabled={!!transmittedName || disabled}
/>
</div>
<div className="columns">
<div className="column">
<Level
right={
<SubmitButton
disabled={disabled || !this.isValid()}
loading={loading}
label={t("branches.create.submit")}
/>
}
/>
</div>
</div>
<div className="columns">
<div className="column">
<Level
right={
<SubmitButton disabled={disabled || !isValid()} loading={loading} label={t("branches.create.submit")} />
}
/>
</div>
</form>
</>
);
}
</div>
</form>
</>
);
};
handleSourceChange = (source: string) => {
this.setState({
source,
});
};
handleNameChange = (name: string) => {
this.setState({
nameValidationError: !validator.isBranchValid(name),
name,
});
};
}
export default withTranslation("repos")(BranchForm);
export default BranchForm;

View File

@@ -45,7 +45,7 @@ class BranchView extends React.Component<Props> {
renderAll={true}
props={{
repository,
branch,
branch
}}
/>
</div>

View File

@@ -40,9 +40,15 @@ const CreateBranch: FC<Props> = ({ repository }) => {
const location = useLocation();
const [t] = useTranslation("repos");
const transmittedName = (url: string) => {
const params = queryString.parse(url);
return params.name;
const transmittedName = (url: string): string | undefined => {
const paramsName = queryString.parse(url).name;
if (paramsName === null) {
return undefined;
}
if (Array.isArray(paramsName)) {
return paramsName[0];
}
return paramsName;
};
if (createdBranch) {
@@ -68,8 +74,7 @@ const CreateBranch: FC<Props> = ({ repository }) => {
<BranchForm
submitForm={create}
loading={isLoadingCreate}
repository={repository}
branches={branches._embedded.branches}
branches={branches._embedded?.branches || []}
transmittedName={transmittedName(location.search)}
/>
</>

View File

@@ -26,36 +26,36 @@ import { orderBranches } from "./orderBranches";
const branch1 = {
name: "branch1",
revision: "revision1",
revision: "revision1"
};
const branch2 = {
name: "branch2",
revision: "revision2",
revision: "revision2"
};
const branch3 = {
name: "branch3",
revision: "revision3",
defaultBranch: true,
defaultBranch: true
};
const defaultBranch = {
name: "default",
revision: "revision4",
defaultBranch: false,
defaultBranch: false
};
const developBranch = {
name: "develop",
revision: "revision5",
defaultBranch: false,
defaultBranch: false
};
const mainBranch = {
name: "main",
revision: "revision6",
defaultBranch: false,
defaultBranch: false
};
const masterBranch = {
name: "master",
revision: "revision7",
defaultBranch: false,
defaultBranch: false
};
describe("order branches", () => {

View File

@@ -48,7 +48,7 @@ describe("filepathSearch tests", () => {
"SomeResolver",
"SomeTokenResolver",
"accesstokenresolver",
"ActorExpression",
"ActorExpression"
];
const matches = filepathSearch(paths, "AcToRe");

View File

@@ -33,10 +33,10 @@ declare global {
export const filepathSearch = (paths: string[], query: string): string[] => {
return paths
.map(createMatcher(query))
.filter((m) => m.matches)
.filter(m => m.matches)
.sort((a, b) => b.score - a.score)
.slice(0, 50)
.map((m) => m.path);
.map(m => m.path);
};
const includes = (value: string, query: string) => {
@@ -58,7 +58,7 @@ export const createMatcher = (query: string) => {
return {
matches: score > 0,
score,
path,
path
};
};
};

View File

@@ -22,16 +22,16 @@
* SOFTWARE.
*/
import React from "react";
import { mount, shallow } from "@scm-manager/ui-tests/enzyme-router";
import "@scm-manager/ui-tests/enzyme";
import "@scm-manager/ui-tests/i18n";
import EditRepoNavLink from "./EditRepoNavLink";
import { mount, shallow } from "@scm-manager/ui-tests";
describe("GeneralNavLink", () => {
it("should render nothing, if the modify link is missing", () => {
const repository = {
_links: {},
namespace: "space",
name: "name",
type: "git",
_links: {}
};
const navLink = shallow(<EditRepoNavLink repository={repository} editUrl="" />);
@@ -40,11 +40,14 @@ describe("GeneralNavLink", () => {
it("should render the navLink", () => {
const repository = {
namespace: "space",
name: "name",
type: "git",
_links: {
update: {
href: "/repositories",
},
},
href: "/repositories"
}
}
};
const navLink = mount(<EditRepoNavLink repository={repository} editUrl="" />);

View File

@@ -21,29 +21,22 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import React, { FC } from "react";
import { useTranslation } from "react-i18next";
import { Repository } from "@scm-manager/ui-types";
import { NavLink } from "@scm-manager/ui-components";
type Props = WithTranslation & {
type Props = {
repository: Repository;
editUrl: string;
};
class EditRepoNavLink extends React.Component<Props> {
isEditable = () => {
return this.props.repository._links.update;
};
render() {
const { editUrl, t } = this.props;
if (!this.isEditable()) {
return null;
}
return <NavLink to={editUrl} label={t("repositoryRoot.menu.generalNavLink")} />;
const EditRepoNavLink: FC<Props> = ({ repository, editUrl }) => {
const [t] = useTranslation("repos");
if (!repository._links.update) {
return null;
}
}
return <NavLink to={editUrl} label={t("repositoryRoot.menu.generalNavLink")} />;
};
export default withTranslation("repos")(EditRepoNavLink);
export default EditRepoNavLink;

View File

@@ -43,7 +43,7 @@ const ImportFromBundleForm: FC<Props> = ({
setCompressed,
password,
setPassword,
disabled,
disabled
}) => {
const [t] = useTranslation("repos");
@@ -53,7 +53,7 @@ const ImportFromBundleForm: FC<Props> = ({
<div className="column is-half is-vcentered">
<LabelWithHelpIcon label={t("import.bundle.title")} helpText={t("import.bundle.helpText")} />
<FileUpload
handleFile={(file) => {
handleFile={file => {
setFile(file);
setValid(!!file);
}}
@@ -74,7 +74,7 @@ const ImportFromBundleForm: FC<Props> = ({
<div className="column is-half is-vcentered">
<InputField
value={password}
onChange={(value) => setPassword(value)}
onChange={value => setPassword(value)}
type="password"
label={t("import.bundle.password.title")}
helpText={t("import.bundle.password.helpText")}

View File

@@ -47,7 +47,7 @@ const ImportFromUrlForm: FC<Props> = ({ repository, onChange, setValid, disabled
const handleImportUrlBlur = (importUrl: string) => {
if (!repository.name) {
// If the repository name is not fill we set a name suggestion
const match = importUrl.match(/([^\/]+?)(?:.git)?$/);
const match = importUrl.match(/([^/]+?)(?:.git)?$/);
if (match && match[1]) {
onChange({ ...repository, name: match[1] });
}
@@ -71,7 +71,7 @@ const ImportFromUrlForm: FC<Props> = ({ repository, onChange, setValid, disabled
<div className="column is-half px-3">
<InputField
label={t("import.username")}
onChange={(username) => onChange({ ...repository, username })}
onChange={username => onChange({ ...repository, username })}
value={repository.username}
helpText={t("help.usernameHelpText")}
disabled={disabled}
@@ -80,7 +80,7 @@ const ImportFromUrlForm: FC<Props> = ({ repository, onChange, setValid, disabled
<div className="column is-half px-3">
<InputField
label={t("import.password")}
onChange={(password) => onChange({ ...repository, password })}
onChange={password => onChange({ ...repository, password })}
value={repository.password}
type="password"
helpText={t("help.passwordHelpText")}

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC, FormEvent, useEffect, useState } from "react";
import React, { FC, FormEvent, useCallback, useEffect, useState } from "react";
import { Repository, RepositoryCreation, RepositoryType } from "@scm-manager/ui-types";
import { ErrorNotification, Level, SubmitButton } from "@scm-manager/ui-components";
import { useTranslation } from "react-i18next";
@@ -42,35 +42,46 @@ const ImportFullRepository: FC<Props> = ({
setImportPending,
setImportedRepository,
nameForm: NameForm,
informationForm: InformationForm,
informationForm: InformationForm
}) => {
const [repo, setRepo] = useState<RepositoryCreation>({
name: "",
namespace: "",
type: repositoryType.name,
contact: "",
description: "",
contextEntries: [],
description: ""
});
const [password, setPassword] = useState("");
const [valid, setValid] = useState({ namespaceAndName: false, contact: true, file: false });
const [file, setFile] = useState<File | null>(null);
const [t] = useTranslation("repos");
const { importFullRepository, importedRepository, isLoading, error } = useImportFullRepository(repositoryType);
const setContactValid = useCallback((contact: boolean) => setValid(currentValid => ({ ...currentValid, contact })), [
setValid
]);
const setNamespaceAndNameValid = useCallback(
(namespaceAndName: boolean) => setValid(currentValid => ({ ...currentValid, namespaceAndName })),
[setValid]
);
const setFileValid = useCallback((file: boolean) => setValid(currentValid => ({ ...currentValid, file })), [
setValid
]);
useEffect(() => setRepo({ ...repo, type: repositoryType.name }), [repositoryType]);
useEffect(() => setImportPending(isLoading), [isLoading]);
useEffect(() => setImportPending(isLoading), [isLoading, setImportPending]);
useEffect(() => {
if (importedRepository) {
setImportedRepository(importedRepository);
}
}, [importedRepository]);
}, [importedRepository, setImportedRepository]);
const isValid = () => Object.values(valid).every((v) => v);
const isValid = () => Object.values(valid).every(v => v);
const submit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
importFullRepository(repo, file!, password);
if (!file) {
throw new Error("File is required for import");
}
importFullRepository({ ...repo, type: repositoryType.name }, file, password);
};
return (
@@ -80,20 +91,20 @@ const ImportFullRepository: FC<Props> = ({
setFile={setFile}
password={password}
setPassword={setPassword}
setValid={(file: boolean) => setValid({ ...valid, file })}
setValid={setFileValid}
/>
<hr />
<NameForm
repository={repo}
onChange={setRepo as React.Dispatch<React.SetStateAction<RepositoryCreation>>}
setValid={(namespaceAndName: boolean) => setValid({ ...valid, namespaceAndName })}
setValid={setNamespaceAndNameValid}
disabled={isLoading}
/>
<InformationForm
repository={repo}
onChange={setRepo as React.Dispatch<React.SetStateAction<RepositoryCreation>>}
disabled={isLoading}
setValid={(contact: boolean) => setValid({ ...valid, contact })}
setValid={setContactValid}
/>
<Level
right={<SubmitButton disabled={!isValid()} loading={isLoading} label={t("repositoryForm.submitImport")} />}

View File

@@ -50,7 +50,7 @@ const ImportFullRepositoryForm: FC<Props> = ({ setFile, setValid, password, setP
<div className="column is-half is-vcentered">
<InputField
value={password}
onChange={(value) => setPassword(value)}
onChange={setPassword}
type="password"
label={t("import.bundle.password.title")}
helpText={t("import.bundle.password.helpText")}

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC, FormEvent, useEffect, useState } from "react";
import React, { FC, FormEvent, useCallback, useEffect, useState } from "react";
import { Repository, RepositoryCreation, RepositoryType } from "@scm-manager/ui-types";
import { ErrorNotification, Level, SubmitButton } from "@scm-manager/ui-components";
import { useTranslation } from "react-i18next";
@@ -42,37 +42,49 @@ const ImportRepositoryFromBundle: FC<Props> = ({
setImportPending,
setImportedRepository,
nameForm: NameForm,
informationForm: InformationForm,
informationForm: InformationForm
}) => {
const [repo, setRepo] = useState<RepositoryCreation>({
name: "",
namespace: "",
type: repositoryType.name,
contact: "",
description: "",
contextEntries: [],
description: ""
});
const [password, setPassword] = useState("");
const [valid, setValid] = useState({ namespaceAndName: false, contact: true, file: false });
const [file, setFile] = useState<File | null>(null);
const [compressed, setCompressed] = useState(true);
const [t] = useTranslation("repos");
const { importRepositoryFromBundle, importedRepository, error, isLoading } =
useImportRepositoryFromBundle(repositoryType);
const { importRepositoryFromBundle, importedRepository, error, isLoading } = useImportRepositoryFromBundle(
repositoryType
);
const setContactValid = useCallback((contact: boolean) => setValid(currentValid => ({ ...currentValid, contact })), [
setValid
]);
const setNamespaceAndNameValid = useCallback(
(namespaceAndName: boolean) => setValid(currentValid => ({ ...currentValid, namespaceAndName })),
[setValid]
);
const setFileValid = useCallback((file: boolean) => setValid(currentValid => ({ ...currentValid, file })), [
setValid
]);
useEffect(() => setRepo({ ...repo, type: repositoryType.name }), [repositoryType]);
useEffect(() => setImportPending(isLoading), [isLoading]);
useEffect(() => setImportPending(isLoading), [isLoading, setImportPending]);
useEffect(() => {
if (importedRepository) {
setImportedRepository(importedRepository);
}
}, [importedRepository]);
}, [importedRepository, setImportedRepository]);
const isValid = () => Object.values(valid).every((v) => v);
const isValid = () => Object.values(valid).every(v => v);
const submit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
importRepositoryFromBundle(repo, file!, compressed, password);
if (!file) {
throw new Error("File is required for import");
}
importRepositoryFromBundle({ ...repo, type: repositoryType.name }, file, compressed, password);
};
return (
@@ -80,7 +92,7 @@ const ImportRepositoryFromBundle: FC<Props> = ({
<ErrorNotification error={error} />
<ImportFromBundleForm
setFile={setFile}
setValid={(file: boolean) => setValid({ ...valid, file })}
setValid={setFileValid}
compressed={compressed}
setCompressed={setCompressed}
password={password}
@@ -91,14 +103,14 @@ const ImportRepositoryFromBundle: FC<Props> = ({
<NameForm
repository={repo}
onChange={setRepo as React.Dispatch<React.SetStateAction<RepositoryCreation>>}
setValid={(namespaceAndName: boolean) => setValid({ ...valid, namespaceAndName })}
setValid={setNamespaceAndNameValid}
disabled={isLoading}
/>
<InformationForm
repository={repo}
onChange={setRepo as React.Dispatch<React.SetStateAction<RepositoryCreation>>}
disabled={isLoading}
setValid={(contact: boolean) => setValid({ ...valid, contact })}
setValid={setContactValid}
/>
<Level
right={<SubmitButton disabled={!isValid()} loading={isLoading} label={t("repositoryForm.submitImport")} />}

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC, FormEvent, useEffect, useState } from "react";
import React, { FC, FormEvent, useCallback, useEffect, useState } from "react";
import { Repository, RepositoryCreation, RepositoryType, RepositoryUrlImport } from "@scm-manager/ui-types";
import ImportFromUrlForm from "./ImportFromUrlForm";
import { ErrorNotification, Level, SubmitButton } from "@scm-manager/ui-components";
@@ -42,7 +42,7 @@ const ImportRepositoryFromUrl: FC<Props> = ({
setImportPending,
setImportedRepository,
nameForm: NameForm,
informationForm: InformationForm,
informationForm: InformationForm
}) => {
const [repo, setRepo] = useState<RepositoryUrlImport>({
name: "",
@@ -52,50 +52,54 @@ const ImportRepositoryFromUrl: FC<Props> = ({
description: "",
importUrl: "",
username: "",
password: "",
contextEntries: [],
password: ""
});
const [valid, setValid] = useState({ namespaceAndName: false, contact: true, importUrl: false });
const [t] = useTranslation("repos");
const { importRepositoryFromUrl, importedRepository, error, isLoading } = useImportRepositoryFromUrl(repositoryType);
const setContactValid = useCallback((contact: boolean) => setValid(currentValid => ({ ...currentValid, contact })), [
setValid
]);
const setNamespaceAndNameValid = useCallback(
(namespaceAndName: boolean) => setValid(currentValid => ({ ...currentValid, namespaceAndName })),
[setValid]
);
const setImportUrlValid = useCallback(
(importUrl: boolean) => setValid(currentValid => ({ ...currentValid, importUrl })),
[setValid]
);
useEffect(() => setRepo({ ...repo, type: repositoryType.name }), [repositoryType]);
useEffect(() => setImportPending(isLoading), [isLoading]);
useEffect(() => setImportPending(isLoading), [isLoading, setImportPending]);
useEffect(() => {
if (importedRepository) {
setImportedRepository(importedRepository);
}
}, [importedRepository]);
}, [importedRepository, setImportedRepository]);
const isValid = () => Object.values(valid).every((v) => v);
const isValid = () => Object.values(valid).every(v => v);
const submit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
importRepositoryFromUrl(repo);
importRepositoryFromUrl({ ...repo, type: repositoryType.name });
};
return (
<form onSubmit={submit}>
{error ? <ErrorNotification error={error} /> : null}
<ImportFromUrlForm
repository={repo}
onChange={setRepo}
setValid={(importUrl: boolean) => setValid({ ...valid, importUrl })}
disabled={isLoading}
/>
<ImportFromUrlForm repository={repo} onChange={setRepo} setValid={setImportUrlValid} disabled={isLoading} />
<hr />
<NameForm
repository={repo}
onChange={setRepo as React.Dispatch<React.SetStateAction<RepositoryCreation>>}
setValid={(namespaceAndName: boolean) => setValid({ ...valid, namespaceAndName })}
setValid={setNamespaceAndNameValid}
disabled={isLoading}
/>
<InformationForm
repository={repo}
onChange={setRepo as React.Dispatch<React.SetStateAction<RepositoryCreation>>}
disabled={isLoading}
setValid={(contact: boolean) => setValid({ ...valid, contact })}
setValid={setContactValid}
/>
<Level
right={<SubmitButton disabled={!isValid()} loading={isLoading} label={t("repositoryForm.submitImport")} />}

View File

@@ -38,11 +38,11 @@ const ImportRepositoryTypeSelect: FC<Props> = ({ repositoryTypes, repositoryType
const createSelectOptions = () => {
const options = repositoryTypes
.filter((repoType) => !!repoType._links.import)
.map((repositoryType) => {
.filter(repoType => !!repoType._links.import)
.map(repositoryType => {
return {
label: repositoryType.displayName,
value: repositoryType.name,
value: repositoryType.name
};
});
options.unshift({ label: "", value: "" });
@@ -50,7 +50,7 @@ const ImportRepositoryTypeSelect: FC<Props> = ({ repositoryTypes, repositoryType
};
const onChangeType = (type: string) => {
const repositoryType = repositoryTypes.filter((t) => t.name === type)[0];
const repositoryType = repositoryTypes.filter(t => t.name === type)[0];
setRepositoryType(repositoryType);
};

View File

@@ -68,7 +68,7 @@ const NamespaceAndNameFields: FC<Props> = ({ repository, onChange, setValid, dis
} else {
setValid(false);
}
}, [repository.name, repository.namespace]);
}, [repository.name, repository.namespace, namespaceStrategy, setValid]);
const handleNamespaceChange = (namespace: string) => {
const valid = validator.isNamespaceValid(namespace);
@@ -96,7 +96,7 @@ const NamespaceAndNameFields: FC<Props> = ({ repository, onChange, setValid, dis
errorMessage: t("validation.namespace-invalid"),
validationError: namespaceValidationError,
disabled: disabled,
informationMessage,
informationMessage
};
if (namespaceStrategy === CUSTOM_NAMESPACE_STRATEGY) {

View File

@@ -22,15 +22,14 @@
* SOFTWARE.
*/
import React from "react";
import { mount, shallow } from "@scm-manager/ui-tests/enzyme-router";
import "@scm-manager/ui-tests/enzyme";
import "@scm-manager/ui-tests/i18n";
import { mount, shallow } from "@scm-manager/ui-tests";
import "@scm-manager/ui-tests";
import PermissionsNavLink from "./PermissionsNavLink";
describe("PermissionsNavLink", () => {
it("should render nothing, if the modify link is missing", () => {
const repository = {
_links: {},
_links: {}
};
const navLink = shallow(<PermissionsNavLink repository={repository} permissionUrl="" />);
@@ -41,9 +40,9 @@ describe("PermissionsNavLink", () => {
const repository = {
_links: {
permissions: {
href: "/permissions",
},
},
href: "/permissions"
}
}
};
const navLink = mount(<PermissionsNavLink repository={repository} permissionUrl="" />);

View File

@@ -42,7 +42,7 @@ class RepositoryDetails extends React.Component<Props> {
name="repos.repository-details.information"
renderAll={true}
props={{
repository,
repository
}}
/>
</div>

View File

@@ -58,7 +58,7 @@ const RepositoryInformationForm: FC<Props> = ({ repository, onChange, disabled,
/>
<Textarea
label={t("repository.description")}
onChange={(description) => onChange({ ...repository, description })}
onChange={description => onChange({ ...repository, description })}
value={repository ? repository.description : ""}
helpText={t("help.descriptionHelpText")}
disabled={disabled}

View File

@@ -22,8 +22,8 @@
* SOFTWARE.
*/
import React from "react";
import { mount, shallow } from "@scm-manager/ui-tests/enzyme-router";
import "@scm-manager/ui-tests/i18n";
import { mount, shallow } from "@scm-manager/ui-tests";
import "@scm-manager/ui-tests";
import RepositoryNavLink from "./RepositoryNavLink";
describe("RepositoryNavLink", () => {
@@ -32,7 +32,7 @@ describe("RepositoryNavLink", () => {
namespace: "Namespace",
name: "Repo",
type: "GIT",
_links: {},
_links: {}
};
const navLink = shallow(
@@ -54,9 +54,9 @@ describe("RepositoryNavLink", () => {
type: "GIT",
_links: {
sources: {
href: "/sources",
},
},
href: "/sources"
}
}
};
const navLink = mount(

View File

@@ -24,13 +24,14 @@
import React from "react";
import { Repository } from "@scm-manager/ui-types";
import { NavLink } from "@scm-manager/ui-components";
import { RouteProps } from "react-router-dom";
type Props = {
repository: Repository;
to: string;
label: string;
linkName: string;
activeWhenMatch?: (route: any) => boolean;
activeWhenMatch?: (route: RouteProps) => boolean;
activeOnlyWhenExact: boolean;
icon?: string;
title?: string;

View File

@@ -131,7 +131,7 @@ const ChangesetDetails: FC<Props> = ({ changeset, repository, fileControlFactory
const description = changesets.parseDescription(changeset.description);
const id = <ChangesetId repository={repository} changeset={changeset} link={false} />;
const date = <DateFromNow date={changeset.date} />;
const parents = changeset._embedded.parents?.map((parent: ParentChangeset, index: number) => (
const parents = changeset._embedded?.parents?.map((parent: ParentChangeset, index: number) => (
<ReactLink title={parent.id} to={parent.id} key={index}>
{parent.id.substring(0, 7)}
</ReactLink>
@@ -187,7 +187,7 @@ const ChangesetDetails: FC<Props> = ({ changeset, repository, fileControlFactory
<Button
color="success"
className="tag"
label={(changeset._embedded["tags"]?.length === 0 && t("changeset.tag.create")) || ""}
label={(changeset._embedded?.tags?.length === 0 && t("changeset.tag.create")) || ""}
icon="plus"
action={() => setTagCreationModalVisible(true)}
/>

View File

@@ -40,7 +40,7 @@ const ChangesetShortLink: (changeset: Changeset, value: string) => Replacement[]
const link = `/repo/${namespace}/${name}/code/changeset/${revision}`;
replacements.push({
textToReplace: m[0],
replacement: <Link to={link}>{m[0]}</Link>,
replacement: <Link to={link}>{m[0]}</Link>
});
m = regex.exec(value);
}

View File

@@ -62,6 +62,16 @@ const Contributor: FC<{ person: Person }> = ({ person }) => {
return <>{person.name}</>;
};
const getUnique = (items: string[]) =>
Object.keys(
items.reduce((result, item) => {
if (!(item in result)) {
result[item] = true;
}
return result;
}, {} as { [type: string]: boolean })
);
const ContributorTable: FC<Props> = ({ changeset }) => {
const [t] = useTranslation("plugins");
@@ -69,11 +79,11 @@ const ContributorTable: FC<Props> = ({ changeset }) => {
if (!changeset.contributors) {
return [];
}
return new Set(changeset.contributors.map((contributor) => contributor.type));
return getUnique(changeset.contributors.map(contributor => contributor.type));
};
const getPersonsByContributorType = (type: string) => {
return changeset.contributors?.filter((contributor) => contributor.type === type).map((t) => t.person);
return changeset.contributors?.filter(contributor => contributor.type === type).map(t => t.person);
};
const getContributorsByType = () => {
@@ -94,12 +104,12 @@ const ContributorTable: FC<Props> = ({ changeset }) => {
<Contributor person={changeset.author} />
</td>
</tr>
{getContributorsByType().map((contributor) => (
{getContributorsByType().map(contributor => (
<tr key={contributor.type}>
<SizedTd>{t("changeset.contributor.type." + contributor.type)}:</SizedTd>
<td className="is-ellipsis-overflow m-0">
<CommaSeparatedList>
{contributor.persons!.map((person) => (
{contributor.persons?.map(person => (
<Contributor key={person.name} person={person} />
))}
</CommaSeparatedList>

View File

@@ -37,12 +37,10 @@ type Props = {
const CreateTagModal: FC<Props> = ({ repository, changeset, onClose }) => {
const { isLoading, error, data: tags } = useTags(repository);
const {
isLoading: isLoadingCreate,
error: errorCreate,
create,
tag: createdTag,
} = useCreateTag(repository, changeset);
const { isLoading: isLoadingCreate, error: errorCreate, create, tag: createdTag } = useCreateTag(
repository,
changeset
);
const [t] = useTranslation("repos");
const [newTagName, setNewTagName] = useState("");
useEffect(() => {
@@ -51,7 +49,7 @@ const CreateTagModal: FC<Props> = ({ repository, changeset, onClose }) => {
}
}, [createdTag, onClose]);
const tagNames = tags?._embedded.tags.map((tag) => tag.name);
const tagNames = tags?._embedded.tags.map(tag => tag.name);
let validationError = "";
@@ -76,7 +74,7 @@ const CreateTagModal: FC<Props> = ({ repository, changeset, onClose }) => {
<InputField
name="name"
label={t("tags.create.form.field.name.label")}
onChange={(val) => setNewTagName(val)}
onChange={val => setNewTagName(val)}
value={newTagName}
validationError={!!validationError}
errorMessage={t(validationError)}

View File

@@ -25,7 +25,14 @@ import React, { FC, useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import classNames from "classnames";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
import { IndexResources, Repository, RepositoryType, CUSTOM_NAMESPACE_STRATEGY } from "@scm-manager/ui-types";
import {
CUSTOM_NAMESPACE_STRATEGY,
IndexResources,
Repository,
RepositoryBase,
RepositoryCreation,
RepositoryType
} from "@scm-manager/ui-types";
import { Checkbox, Level, Select, SubmitButton } from "@scm-manager/ui-components";
import NamespaceAndNameFields from "../NamespaceAndNameFields";
import RepositoryInformationForm from "../RepositoryInformationForm";
@@ -40,10 +47,6 @@ type Props = {
indexResources?: IndexResources;
};
type RepositoryCreation = Repository & {
contextEntries: object;
};
const RepositoryForm: FC<Props> = ({
createRepository,
modifyRepository,
@@ -51,29 +54,35 @@ const RepositoryForm: FC<Props> = ({
repositoryTypes,
namespaceStrategy,
loading,
indexResources,
indexResources
}) => {
const [repo, setRepo] = useState<Repository>({
const [repo, setRepo] = useState<RepositoryBase>({
name: "",
namespace: "",
type: "",
contact: "",
description: "",
_links: {},
description: ""
});
const [initRepository, setInitRepository] = useState(false);
const [contextEntries, setContextEntries] = useState({});
const setCreationContextEntry = useCallback(
(key: string, value: any) => {
setContextEntries((entries) => ({
(key: string, value: unknown) => {
setContextEntries(entries => ({
...entries,
[key]: value,
[key]: value
}));
},
[setContextEntries]
);
const [valid, setValid] = useState({ namespaceAndName: true, contact: true });
const [t] = useTranslation("repos");
const setContactValid = useCallback((contact: boolean) => setValid(currentValid => ({ ...currentValid, contact })), [
setValid
]);
const setNamespaceAndNameValid = useCallback(
(namespaceAndName: boolean) => setValid(currentValid => ({ ...currentValid, namespaceAndName })),
[setValid]
);
useEffect(() => {
if (repository) {
@@ -88,7 +97,7 @@ const RepositoryForm: FC<Props> = ({
const isValid = () => {
return (
!(!repo.name || (namespaceStrategy === CUSTOM_NAMESPACE_STRATEGY && !repo.namespace)) &&
Object.values(valid).every((v) => v)
Object.values(valid).every(v => v)
);
};
@@ -98,17 +107,21 @@ const RepositoryForm: FC<Props> = ({
if (createRepository) {
createRepository({ ...repo, contextEntries }, initRepository);
} else if (modifyRepository) {
modifyRepository(repo);
if (!!repository && !!repository._links.update) {
modifyRepository({ ...repository, ...repo });
} else {
throw new Error("Repository has to be present and contain update link for modification");
}
}
}
};
const createSelectOptions = (repositoryTypes?: RepositoryType[]) => {
if (repositoryTypes) {
return repositoryTypes.map((repositoryType) => {
return repositoryTypes.map(repositoryType => {
return {
label: repositoryType.displayName,
value: repositoryType.name,
value: repositoryType.name
};
});
}
@@ -123,21 +136,21 @@ const RepositoryForm: FC<Props> = ({
const extensionProps = {
repository: repo,
setCreationContextEntry: setCreationContextEntry,
indexResources: indexResourcesWithLinks,
indexResources: indexResourcesWithLinks
};
return (
<>
<NamespaceAndNameFields
repository={repo}
onChange={setRepo}
setValid={(namespaceAndName) => setValid({ ...valid, namespaceAndName })}
setValid={setNamespaceAndNameValid}
disabled={disabled}
/>
<div className="columns">
<div className={classNames("column", "is-half")}>
<Select
label={t("repository.type")}
onChange={(type) => setRepo({ ...repo, type })}
onChange={type => setRepo({ ...repo, type })}
value={repo ? repo.type : ""}
options={createSelectOptions(repositoryTypes)}
helpText={t("help.typeHelpText")}
@@ -181,12 +194,7 @@ const RepositoryForm: FC<Props> = ({
return (
<form onSubmit={submit}>
{renderCreateOnlyFields()}
<RepositoryInformationForm
repository={repo}
onChange={setRepo}
disabled={disabled}
setValid={(contact) => setValid({ ...valid, contact })}
/>
<RepositoryInformationForm repository={repo} onChange={setRepo} disabled={disabled} setValid={setContactValid} />
{submitButton()}
</form>
);

View File

@@ -71,7 +71,7 @@ const RepositoryFormSwitcher: FC<Props> = ({ forms }) => (
<TopLevel
right={
<ButtonAddons>
{(forms || []).map((form) => (
{(forms || []).map(form => (
<RepositoryFormButton key={form.path} {...form} />
))}
</ButtonAddons>

View File

@@ -39,7 +39,7 @@ describe("repository name validation", () => {
it("should allow same names as the backend", () => {
const validPaths = ["scm", "scm.gitz", "s", "sc", ".hiddenrepo", "b.", "...", "..c", "d..", "a..c"];
validPaths.forEach((path) => expect(validator.isNameValid(path)).toBe(true));
validPaths.forEach(path => expect(validator.isNameValid(path)).toBe(true));
});
it("should deny same names as the backend", () => {
@@ -68,6 +68,7 @@ describe("repository name validation", () => {
"scm//main",
"scm\\main",
"scm/main-$HOME",
// eslint-disable-next-line no-template-curly-in-string
"scm/main-${HOME}-home",
"scm/main-%HOME-home",
"scm/main-%HOME%-home",
@@ -92,10 +93,10 @@ describe("repository name validation", () => {
"scm/main",
"scm/plugins/git-plugin",
"scm/plugins/git-plugin",
"scm.git",
"scm.git"
];
invalidPaths.forEach((path) => expect(validator.isNameValid(path)).toBe(false));
invalidPaths.forEach(path => expect(validator.isNameValid(path)).toBe(false));
});
});

View File

@@ -25,6 +25,7 @@
import { validation } from "@scm-manager/ui-components";
import { isNameValid as isUserNameValid } from "../../../users/components/userValidation";
// eslint-disable-next-line
const nameRegex = /(?!^\.\.$)(?!^\.$)(?!.*[.]git$)(?!.*[\\\[\]])^[A-Za-z0-9\.][A-Za-z0-9\.\-_]*$/;
const namespaceExceptionsRegex = /^(([0-9]{1,3})|(create)|(import))$/;

View File

@@ -50,10 +50,10 @@ class RepositoryList extends React.Component<Props> {
props={{
page,
search,
namespace,
namespace
}}
/>
{groups.map((group) => {
{groups.map(group => {
return <RepositoryGroupEntry group={group} key={group.name} />;
})}
</div>

View File

@@ -26,42 +26,42 @@ import groupByNamespace from "./groupByNamespace";
const base = {
type: "git",
_links: {},
_links: {}
};
const slartiBlueprintsFjords = {
...base,
namespace: "slarti",
name: "fjords-blueprints",
name: "fjords-blueprints"
};
const slartiFjords = {
...base,
namespace: "slarti",
name: "fjords",
name: "fjords"
};
const hitchhikerRestand = {
...base,
namespace: "hitchhiker",
name: "restand",
name: "restand"
};
const hitchhikerPuzzle42 = {
...base,
namespace: "hitchhiker",
name: "puzzle42",
name: "puzzle42"
};
const hitchhikerHeartOfGold = {
...base,
namespace: "hitchhiker",
name: "heartOfGold",
name: "heartOfGold"
};
const zaphodMarvinFirmware = {
...base,
namespace: "zaphod",
name: "marvin-firmware",
name: "marvin-firmware"
};
it("should group the repositories by their namespace", () => {
@@ -71,30 +71,30 @@ it("should group the repositories by their namespace", () => {
hitchhikerRestand,
slartiFjords,
hitchhikerHeartOfGold,
hitchhikerPuzzle42,
hitchhikerPuzzle42
];
const namespaces = {
_embedded: {
namespaces: [{ namespace: "hitchhiker" }, { namespace: "slarti" }, { namespace: "zaphod" }],
},
namespaces: [{ namespace: "hitchhiker" }, { namespace: "slarti" }, { namespace: "zaphod" }]
}
};
const expected = [
{
name: "hitchhiker",
namespace: { namespace: "hitchhiker" },
repositories: [hitchhikerHeartOfGold, hitchhikerPuzzle42, hitchhikerRestand],
repositories: [hitchhikerHeartOfGold, hitchhikerPuzzle42, hitchhikerRestand]
},
{
name: "slarti",
namespace: { namespace: "slarti" },
repositories: [slartiFjords, slartiBlueprintsFjords],
repositories: [slartiFjords, slartiBlueprintsFjords]
},
{
name: "zaphod",
namespace: { namespace: "zaphod" },
repositories: [zaphodMarvinFirmware],
},
repositories: [zaphodMarvinFirmware]
}
];
expect(groupByNamespace(repositories, namespaces)).toEqual(expected);

View File

@@ -38,7 +38,7 @@ export default function groupByNamespace(
group = {
name: groupName,
namespace: namespace,
repositories: [],
repositories: []
};
groups[groupName] = group;
}
@@ -65,5 +65,5 @@ function sortByName(a, b) {
}
function findNamespace(namespaces: NamespaceCollection, namespaceToFind: string) {
return namespaces._embedded.namespaces.find((namespace) => namespace.namespace === namespaceToFind);
return namespaces._embedded.namespaces.find(namespace => namespace.namespace === namespaceToFind);
}

Some files were not shown because too many files have changed in this diff Show More