mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-02-28 09:20:52 +01:00
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:
2
gradle/changelog/enforce_eslint.yaml
Normal file
2
gradle/changelog/enforce_eslint.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: changed
|
||||
description: Enforce eslint to ensure accessible html ([#1878](https://github.com/scm-manager/scm-manager/pull/1878))
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
27
scm-ui/ui-tests/index.ts
Normal 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";
|
||||
32
scm-ui/ui-types/src/General.ts
Normal file
32
scm-ui/ui-types/src/General.ts
Normal 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;
|
||||
@@ -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 & {
|
||||
|
||||
@@ -71,3 +71,4 @@ export * from "./ApiKeys";
|
||||
export * from "./PublicKeys";
|
||||
export * from "./GlobalPermissions";
|
||||
export * from "./Search";
|
||||
export * from "./General";
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
"systemjs": "0.21.6"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "jest"
|
||||
"test": "jest",
|
||||
"lint": "eslint src"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scm-manager/jest-preset": "^2.13.0",
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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");
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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} />;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function groupByCategory(plugins: Plugin[]): PluginGroup[] {
|
||||
if (!group) {
|
||||
group = {
|
||||
name: groupName,
|
||||
plugins: [],
|
||||
plugins: []
|
||||
};
|
||||
groups[groupName] = group;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ class PermissionRoleDetails extends React.Component<Props> {
|
||||
name="repositoryRole.role-details.information"
|
||||
renderAll={true}
|
||||
props={{
|
||||
role,
|
||||
role
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
|
||||
@@ -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>) => {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -50,7 +50,7 @@ const SingleRepositoryRole: FC = () => {
|
||||
|
||||
const extensionProps = {
|
||||
role,
|
||||
url,
|
||||
url
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ const LoginButton: FC<Props> = ({ burgerMode, links, className }) => {
|
||||
from,
|
||||
to,
|
||||
className: headerButtonContentClassName,
|
||||
content,
|
||||
content
|
||||
};
|
||||
|
||||
if (links?.login) {
|
||||
|
||||
@@ -48,7 +48,7 @@ const LogoutButton: FC<Props> = ({ burgerMode, links, className }) => {
|
||||
label,
|
||||
|
||||
className: headerButtonContentClassName,
|
||||
content,
|
||||
content
|
||||
};
|
||||
|
||||
if (links?.logout) {
|
||||
|
||||
@@ -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 = "/";
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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" />);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -45,7 +45,7 @@ class BranchView extends React.Component<Props> {
|
||||
renderAll={true}
|
||||
props={{
|
||||
repository,
|
||||
branch,
|
||||
branch
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -48,7 +48,7 @@ describe("filepathSearch tests", () => {
|
||||
"SomeResolver",
|
||||
"SomeTokenResolver",
|
||||
"accesstokenresolver",
|
||||
"ActorExpression",
|
||||
"ActorExpression"
|
||||
];
|
||||
|
||||
const matches = filepathSearch(paths, "AcToRe");
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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="" />);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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")} />}
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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")} />}
|
||||
|
||||
@@ -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")} />}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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="" />);
|
||||
|
||||
@@ -42,7 +42,7 @@ class RepositoryDetails extends React.Component<Props> {
|
||||
name="repos.repository-details.information"
|
||||
renderAll={true}
|
||||
props={{
|
||||
repository,
|
||||
repository
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -71,7 +71,7 @@ const RepositoryFormSwitcher: FC<Props> = ({ forms }) => (
|
||||
<TopLevel
|
||||
right={
|
||||
<ButtonAddons>
|
||||
{(forms || []).map((form) => (
|
||||
{(forms || []).map(form => (
|
||||
<RepositoryFormButton key={form.path} {...form} />
|
||||
))}
|
||||
</ButtonAddons>
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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))$/;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user