diff --git a/docs/de/user/repo/assets/create-repository.png b/docs/de/user/repo/assets/create-repository.png index 2f314dcef5..4fa11f438e 100644 Binary files a/docs/de/user/repo/assets/create-repository.png and b/docs/de/user/repo/assets/create-repository.png differ diff --git a/docs/de/user/repo/index.md b/docs/de/user/repo/index.md index a80cb2108d..a8de61a347 100644 --- a/docs/de/user/repo/index.md +++ b/docs/de/user/repo/index.md @@ -26,7 +26,12 @@ Im SCM-Manager können neue Git, Mercurial & Subersion (SVN) Repositories über Optional kann man das Repository beim Erstellen direkt initialisieren. Damit werden für Git und Mercurial jeweils der Standard-Branch (master bzw. default) angelegt. Außerdem wird ein initialer Commit ausgeführt, der eine README.md erzeugt. Für Subversion Repositories wird die README.md in einen Ordner `trunk` abgelegt. -Ist die Namespace-Strategie auf "Benutzerdefiniert" eingestellt, muss noch ein Namespace eingetragen werden. Für den Namespace gelten dieselben Regeln wie für den Namen des Repositories. Darüber hinaus darf ein Namespace nicht nur aus bis zu drei Ziffern (z. B. "123") oder den Wörter "create" und "import" bestehen. +Ist die Namespace-Strategie auf "Benutzerdefiniert" eingestellt, muss noch ein Namespace eingetragen werden. +Für den Namespace gelten dieselben Regeln wie für den Namen des Repositories. Darüber hinaus darf ein Namespace +nicht nur aus bis zu drei Ziffern (z. B. "123") oder den Wörter "create" und "import" bestehen. +Bei der Eingabe werden nach den ersten Zeichen bereits bestehende passende Werte vorgeschlagen, sodass diese leichter +übernommen werden können. Ein neuer Namespace muss explizit mit dem entsprechenden Eintrag in der Vorschlagsliste +neu erstellt werden. ![Repository erstellen](assets/create-repository.png) diff --git a/docs/en/user/repo/assets/create-repository.png b/docs/en/user/repo/assets/create-repository.png index 1d1874a0b3..2f3b10646f 100644 Binary files a/docs/en/user/repo/assets/create-repository.png and b/docs/en/user/repo/assets/create-repository.png differ diff --git a/docs/en/user/repo/index.md b/docs/en/user/repo/index.md index e72becc28b..60d1ad9e06 100644 --- a/docs/en/user/repo/index.md +++ b/docs/en/user/repo/index.md @@ -24,7 +24,10 @@ In SCM-Manager new Git, Mercurial & Subversion (SVN) repositories can be created Optionally, repositories can be initialized during the creation. That creates a standard branch (master or default) for Git and Mercurial repositories. Additionally, it performs a commit that creates a README.md. For Subversion repositories the README.md will be created in a directory named `trunk`. -If the namespace strategy is set to custom, the namespace field is also mandatory. The namespace must heed the same restrictions as the name. Additionally, namespaces that only consist of three digits, or the words "create" and "import" are not valid. +If the namespace strategy is set to custom, the namespace field is also mandatory. The namespace must heed the same +restrictions as the name. Additionally, namespaces that only consist of three digits, or the words "create" +and "import" are not valid. After typing the first characters, existing matching namespaces are suggested which can +be chosen. To create a new namespace, this has to be chosen from the drop down explicitly. ![Create Repository](assets/create-repository.png) diff --git a/gradle/changelog/autocomplete_namespaces.yaml b/gradle/changelog/autocomplete_namespaces.yaml new file mode 100644 index 0000000000..268c020769 --- /dev/null +++ b/gradle/changelog/autocomplete_namespaces.yaml @@ -0,0 +1,2 @@ +- type: changed + description: Autocompletion for namespaces ([#1916](https://github.com/scm-manager/scm-manager/pull/1916)) diff --git a/scm-ui/ui-api/src/index.ts b/scm-ui/ui-api/src/index.ts index e13c174c25..5ca6572913 100644 --- a/scm-ui/ui-api/src/index.ts +++ b/scm-ui/ui-api/src/index.ts @@ -36,6 +36,7 @@ export * from "./users"; export * from "./suggestions"; export * from "./userSuggestions"; export * from "./groupSuggestions"; +export * from "./namespaceSuggestions"; export * from "./repositories"; export * from "./namespaces"; export * from "./branches"; diff --git a/scm-ui/ui-api/src/namespaceSuggestions.ts b/scm-ui/ui-api/src/namespaceSuggestions.ts new file mode 100644 index 0000000000..9080271e11 --- /dev/null +++ b/scm-ui/ui-api/src/namespaceSuggestions.ts @@ -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 { Link } from "@scm-manager/ui-types"; +import { useIndexLinks } from "./base"; +import { useSuggestions } from "./suggestions"; + +export const useNamespaceSuggestions = () => { + const indexLinks = useIndexLinks(); + const autocompleteLink = (indexLinks.autocomplete as Link[]).find((i) => i.name === "namespaces"); + return useSuggestions(autocompleteLink?.href); +}; diff --git a/scm-ui/ui-components/src/Autocomplete.tsx b/scm-ui/ui-components/src/Autocomplete.tsx index b54c2a02bc..80d4cfdeeb 100644 --- a/scm-ui/ui-components/src/Autocomplete.tsx +++ b/scm-ui/ui-components/src/Autocomplete.tsx @@ -37,8 +37,11 @@ type Props = { placeholder: string; loadingMessage: string; noOptionsMessage: string; + errorMessage?: string; + informationMessage?: string; creatable?: boolean; className?: string; + disabled?: boolean; }; type State = {}; @@ -78,8 +81,11 @@ class Autocomplete extends React.Component { loadingMessage, noOptionsMessage, loadSuggestions, + errorMessage, + informationMessage, creatable, - className + className, + disabled } = this.props; return ( @@ -108,6 +114,7 @@ class Autocomplete extends React.Component { }); }} aria-label={helpText || label} + isDisabled={disabled} /> ) : ( { loadingMessage={() => loadingMessage} noOptionsMessage={() => noOptionsMessage} aria-label={helpText || label} + isDisabled={disabled} /> )} + {errorMessage ?

{errorMessage}

: null} + {informationMessage ?

{informationMessage}

: null} ); } diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap index 8f04dae547..f5f2111693 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -19219,6 +19219,10 @@ exports[`Storyshots Modal/Modal With form elements 1`] = `null`; exports[`Storyshots Modal/Modal With long tooltips 1`] = `null`; +exports[`Storyshots Modal/Modal With overflow 1`] = `null`; + +exports[`Storyshots Modal/Modal With overflow and footer 1`] = `null`; + exports[`Storyshots Notification Closeable 1`] = `
{}; +const doNothing = () => { + // nothing to do +}; const withFormElementsBody = ( <> @@ -71,8 +75,21 @@ const withFormElementsFooter = ( ); +const loadSuggestions: (p: string) => Promise = () => + new Promise(resolve => { + setTimeout(() => { + resolve([ + { value: { id: "trillian", displayName: "Tricia McMillan" }, label: "Tricia McMillan" }, + { value: { id: "zaphod", displayName: "Zaphod Beeblebrox" }, label: "Zaphod Beeblebrox" }, + { value: { id: "ford", displayName: "Ford Prefect" }, label: "Ford Prefect" }, + { value: { id: "dent", displayName: "Arthur Dent" }, label: "Arthur Dent" }, + { value: { id: "marvin", displayName: "Marvin" }, label: "Marvin the Paranoid Android " } + ]); + }); + }); + storiesOf("Modal/Modal", module) - .addDecorator((story) => {story()}) + .addDecorator(story => {story()}) .add("Default", () => (

{text}

@@ -104,7 +121,7 @@ storiesOf("Modal/Modal", module) This story exists because we had a problem, that long tooltips causes a horizontal scrollbar on the modal.
-

The following elements will have a verly long help text, which has triggered the scrollbar in the past.

+

The following elements will have a very long help text, which has triggered the scrollbar in the past.


@@ -211,10 +228,47 @@ storiesOf("Modal/Modal", module)

- )); + )) + .add("With overflow", () => { + return ( + +

Please Select

+ { + // nothing to do + }} + loadSuggestions={loadSuggestions} + /> +
+ ); + }) + .add("With overflow and footer", () => { + return ( + +

Please Select

+ { + // nothing to do + }} + loadSuggestions={loadSuggestions} + /> +
+ ); + }); -const NonCloseableModal: FC = ({ children }) => { - return ; +type NonCloseableModalProps = { overflowVisible?: boolean; footer?: any }; + +const NonCloseableModal: FC = ({ overflowVisible, footer, children }) => { + return ( + + ); }; const CloseableModal: FC = ({ children }) => { diff --git a/scm-ui/ui-components/src/modals/Modal.tsx b/scm-ui/ui-components/src/modals/Modal.tsx index bcda9f486b..d3a6ef3d81 100644 --- a/scm-ui/ui-components/src/modals/Modal.tsx +++ b/scm-ui/ui-components/src/modals/Modal.tsx @@ -42,10 +42,24 @@ type Props = { headColor?: string; headTextColor?: string; size?: ModalSize; + overflowVisible?: boolean; }; -const SizedModal = styled.div<{ size?: ModalSize }>` +const SizedModal = styled.div<{ size?: ModalSize; overflow: string }>` width: ${props => (props.size ? `${modalSizes[props.size]}%` : "640px")}; + overflow: ${props => props.overflow}; +`; + +const DivWithOptionalOverflow = styled.div<{ overflow: string; borderBottomRadius: string }>` + overflow: ${props => props.overflow}; + border-bottom-left-radius: ${props => props.borderBottomRadius}; + border-bottom-right-radius: ${props => props.borderBottomRadius}; +`; + +const SectionWithOptionalOverflow = styled.section<{ overflow: string; borderBottomRadius: string }>` + overflow: ${props => props.overflow}; + border-bottom-left-radius: ${props => props.borderBottomRadius}; + border-bottom-right-radius: ${props => props.borderBottomRadius}; `; export const Modal: FC = ({ @@ -57,7 +71,8 @@ export const Modal: FC = ({ className, headColor = "secondary-less", headTextColor = "secondary-most", - size + size, + overflowVisible }) => { const portalRootElement = usePortalRootElement("modalsRoot"); const initialFocusRef = useRef(null); @@ -85,18 +100,29 @@ export const Modal: FC = ({ } }; + const overflowAttribute = overflowVisible ? "visible" : "auto"; + const borderBottomRadiusAttribute = overflowVisible && !footer ? "inherit" : "unset"; + const modalElement = ( -
+
- +

{title}

-
{body}
+ + {body} + {showFooter}
-
+
); return ReactDOM.createPortal(modalElement, portalRootElement); diff --git a/scm-ui/ui-types/src/Autocomplete.ts b/scm-ui/ui-types/src/Autocomplete.ts index d237088d1d..33607db776 100644 --- a/scm-ui/ui-types/src/Autocomplete.ts +++ b/scm-ui/ui-types/src/Autocomplete.ts @@ -24,7 +24,7 @@ export type AutocompleteObject = { id: string; - displayName: string; + displayName?: string; }; export type SelectValue = { diff --git a/scm-ui/ui-webapp/src/repos/components/NamespaceAndNameFields.tsx b/scm-ui/ui-webapp/src/repos/components/NamespaceAndNameFields.tsx index 5dbd2a949f..50348d5d4d 100644 --- a/scm-ui/ui-webapp/src/repos/components/NamespaceAndNameFields.tsx +++ b/scm-ui/ui-webapp/src/repos/components/NamespaceAndNameFields.tsx @@ -23,12 +23,12 @@ */ import React, { FC, useEffect, useState } from "react"; -import { CUSTOM_NAMESPACE_STRATEGY, RepositoryCreation } from "@scm-manager/ui-types"; +import { CUSTOM_NAMESPACE_STRATEGY, RepositoryCreation} from "@scm-manager/ui-types"; import { useTranslation } from "react-i18next"; import { InputField } from "@scm-manager/ui-components"; -import { ExtensionPoint } from "@scm-manager/ui-extensions"; import * as validator from "./form/repositoryValidation"; -import { useNamespaceStrategies } from "@scm-manager/ui-api"; +import { useNamespaceStrategies} from "@scm-manager/ui-api"; +import NamespaceInput from "./NamespaceInput"; type Props = { repository: RepositoryCreation; @@ -82,30 +82,6 @@ const NamespaceAndNameFields: FC = ({ repository, onChange, setValid, dis onChange({ ...repository, name }); }; - const renderNamespaceField = () => { - let informationMessage = undefined; - if (repository?.namespace?.indexOf(" ") > 0) { - informationMessage = t("validation.namespaceSpaceWarningText"); - } - - const props = { - label: t("repository.namespace"), - helpText: t("help.namespaceHelpText"), - value: repository ? repository.namespace : "", - onChange: handleNamespaceChange, - errorMessage: t("validation.namespace-invalid"), - validationError: namespaceValidationError, - disabled: disabled, - informationMessage - }; - - if (namespaceStrategy === CUSTOM_NAMESPACE_STRATEGY) { - return ; - } - - return ; - }; - // not yet loaded if (namespaceStrategy === "") { return null; @@ -113,7 +89,13 @@ const NamespaceAndNameFields: FC = ({ repository, onChange, setValid, dis return ( <> - {renderNamespaceField()} + void; + namespaceStrategy?: string; + namespaceValidationError?: boolean; + disabled?: boolean; +}; + +const NamespaceInput: FC = ({ + namespace, + handleNamespaceChange, + namespaceStrategy, + namespaceValidationError, + disabled +}) => { + const [t] = useTranslation("repos"); + const loadNamespaceSuggestions = useNamespaceSuggestions(); + + let informationMessage = undefined; + if (namespace?.indexOf(" ") > 0) { + informationMessage = t("validation.namespaceSpaceWarningText"); + } + + const repositorySelectValue = namespace ? { value: { id: namespace, displayName: "" }, label: namespace } : undefined; + const props = { + label: t("repository.namespace"), + helpText: t("help.namespaceHelpText"), + value: namespace, + onChange: handleNamespaceChange, + errorMessage: namespaceValidationError ? t("validation.namespace-invalid") : "", + informationMessage: informationMessage, + validationError: namespaceValidationError, + disabled: disabled + }; + + if (namespaceStrategy === CUSTOM_NAMESPACE_STRATEGY) { + return ( + handleNamespaceChange(namespaceValue.value.id)} + placeholder={""} + creatable={true} + /> + ); + } + + return ; +}; + +export default NamespaceInput; diff --git a/scm-ui/ui-webapp/src/repos/containers/RenameRepository.tsx b/scm-ui/ui-webapp/src/repos/containers/RenameRepository.tsx index 29ed94eeb7..762e5d349d 100644 --- a/scm-ui/ui-webapp/src/repos/containers/RenameRepository.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/RenameRepository.tsx @@ -23,13 +23,18 @@ */ import React, { FC, useState } from "react"; -import { CUSTOM_NAMESPACE_STRATEGY, Repository } from "@scm-manager/ui-types"; +import { Repository } from "@scm-manager/ui-types"; import { Button, ButtonGroup, ErrorNotification, InputField, Level, Loading, Modal } from "@scm-manager/ui-components"; import { useTranslation } from "react-i18next"; import { Redirect } from "react-router-dom"; -import { ExtensionPoint } from "@scm-manager/ui-extensions"; import * as validator from "../components/form/repositoryValidation"; import { useNamespaceStrategies, useRenameRepository } from "@scm-manager/ui-api"; +import NamespaceInput from "../components/NamespaceInput"; +import styled from "styled-components"; + +const WithOverflow = styled.div` + overflow: visible; +`; type Props = { repository: Repository; @@ -76,25 +81,8 @@ const RenameRepository: FC = ({ repository }) => { setName(name); }; - const renderNamespaceField = () => { - const props = { - label: t("repository.namespace"), - helpText: t("help.namespaceHelpText"), - value: namespace, - onChange: handleNamespaceChange, - errorMessage: t("validation.namespace-invalid"), - validationError: namespaceValidationError - }; - - if (namespaceStrategies?.current === CUSTOM_NAMESPACE_STRATEGY) { - return ; - } - - return ; - }; - const modalBody = ( -
+ {renamingError ? : null} = ({ repository }) => { value={name} onChange={handleNameChange} /> - {renderNamespaceField()} -
+ + ); const footer = ( @@ -137,6 +130,7 @@ const RenameRepository: FC = ({ repository }) => { footer={footer} body={modalBody} closeFunction={() => setShowModal(false)} + overflowVisible={true} /> ); diff --git a/scm-webapp/src/main/java/sonia/scm/GenericDisplayManager.java b/scm-webapp/src/main/java/sonia/scm/GenericDisplayManager.java index af157f92ed..15f8871842 100644 --- a/scm-webapp/src/main/java/sonia/scm/GenericDisplayManager.java +++ b/scm-webapp/src/main/java/sonia/scm/GenericDisplayManager.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm; import sonia.scm.search.SearchRequest; @@ -32,7 +32,6 @@ import java.util.Optional; import java.util.function.Function; import static java.util.Optional.ofNullable; -import static sonia.scm.group.DisplayGroup.from; public abstract class GenericDisplayManager implements DisplayManager { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteResource.java index 8fbb8012b0..3bd20fc4f7 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteResource.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; import io.swagger.v3.oas.annotations.OpenAPIDefinition; @@ -32,6 +32,9 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import sonia.scm.ReducedModelObject; import sonia.scm.group.GroupDisplayManager; +import sonia.scm.repository.NamespaceManager; +import sonia.scm.search.SearchRequest; +import sonia.scm.search.SearchUtil; import sonia.scm.user.UserDisplayManager; import sonia.scm.web.VndMediaType; @@ -46,6 +49,8 @@ import java.util.Collection; import java.util.List; import java.util.stream.Collectors; +import static sonia.scm.DisplayManager.DEFAULT_LIMIT; + @OpenAPIDefinition(tags = { @Tag(name = "Autocomplete", description = "Autocomplete related endpoints") }) @@ -58,16 +63,18 @@ public class AutoCompleteResource { public static final String INVALID_PARAMETER_LENGTH = "Invalid parameter length."; - private ReducedObjectModelToDtoMapper mapper; + private final ReducedObjectModelToDtoMapper mapper; - private UserDisplayManager userDisplayManager; - private GroupDisplayManager groupDisplayManager; + private final UserDisplayManager userDisplayManager; + private final GroupDisplayManager groupDisplayManager; + private final NamespaceManager namespaceManager; @Inject - public AutoCompleteResource(ReducedObjectModelToDtoMapper mapper, UserDisplayManager userDisplayManager, GroupDisplayManager groupDisplayManager) { + public AutoCompleteResource(ReducedObjectModelToDtoMapper mapper, UserDisplayManager userDisplayManager, GroupDisplayManager groupDisplayManager, NamespaceManager namespaceManager) { this.mapper = mapper; this.userDisplayManager = userDisplayManager; this.groupDisplayManager = groupDisplayManager; + this.namespaceManager = namespaceManager; } @GET @@ -123,12 +130,50 @@ public class AutoCompleteResource { return map(groupDisplayManager.autocomplete(filter)); } + @GET + @Path("namespaces") + @Produces(VndMediaType.AUTOCOMPLETE) + @Operation(summary = "Search namespaces", description = "Returns matching namespaces.", tags = "Autocomplete") + @ApiResponse( + responseCode = "200", + description = "success", + content = @Content( + mediaType = VndMediaType.AUTOCOMPLETE, + schema = @Schema(implementation = ReducedObjectModelDto.class) + )) + @ApiResponse(responseCode = "400", description = "if the searched string contains less than 2 characters") + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + ) + ) + public List searchNamespace(@NotEmpty(message = PARAMETER_IS_REQUIRED) @Size(min = MIN_SEARCHED_CHARS, message = INVALID_PARAMETER_LENGTH) @QueryParam("q") String filter) { + SearchRequest searchRequest = new SearchRequest(filter, true, DEFAULT_LIMIT); + return map(SearchUtil.search( + searchRequest, + namespaceManager.getAll(), + namespace -> SearchUtil.matchesOne(searchRequest, namespace.getNamespace()) ? new ReducedModelObject() { + @Override + public String getId() { + return namespace.getId(); + } + + @Override + public String getDisplayName() { + return null; + } + } : null + )); + } + private List map(Collection autocomplete) { return autocomplete .stream() .map(mapper::map) .collect(Collectors.toList()); } - - } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java index 2a2532ac61..0deea54e22 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java @@ -121,6 +121,7 @@ public class IndexDtoGenerator extends HalAppenderMapper { if (GroupPermissions.autocomplete().isPermitted()) { autoCompleteLinks.add(Link.linkBuilder("autocomplete", resourceLinks.autoComplete().groups()).withName("groups").build()); } + autoCompleteLinks.add(Link.linkBuilder("autocomplete", resourceLinks.autoComplete().namespaces()).withName("namespaces").build()); builder.array(autoCompleteLinks); if (GroupPermissions.list().isPermitted()) { builder.single(link("groups", resourceLinks.groupCollection().self())); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index 8ec0177ebd..5de7e80a00 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -317,6 +317,10 @@ class ResourceLinks { String groups() { return linkBuilder.method("searchGroup").parameters().href(); } + + String namespaces() { + return linkBuilder.method("searchNamespace").parameters().href(); + } } ConfigLinks config() { diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AutoCompleteResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AutoCompleteResourceTest.java index 191c469d4a..ed2c689f7b 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AutoCompleteResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AutoCompleteResourceTest.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; import com.fasterxml.jackson.databind.ObjectMapper; @@ -36,11 +36,14 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.DisplayManager; import sonia.scm.group.DefaultGroupDisplayManager; import sonia.scm.group.Group; import sonia.scm.group.xml.XmlGroupDAO; +import sonia.scm.repository.Namespace; +import sonia.scm.repository.NamespaceManager; import sonia.scm.store.ConfigurationStore; import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.user.DefaultUserDisplayManager; @@ -56,6 +59,7 @@ import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; +import static java.util.Arrays.asList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -78,6 +82,8 @@ public class AutoCompleteResourceTest { private XmlUserDAO userDao; private XmlGroupDAO groupDao; + @Mock + private NamespaceManager namespaceManager; private XmlDatabase xmlDB; private ObjectMapper jsonObjectMapper = new ObjectMapper(); @@ -97,7 +103,7 @@ public class AutoCompleteResourceTest { ReducedObjectModelToDtoMapperImpl mapper = new ReducedObjectModelToDtoMapperImpl(); DefaultUserDisplayManager userManager = new DefaultUserDisplayManager(this.userDao); DefaultGroupDisplayManager groupManager = new DefaultGroupDisplayManager(groupDao); - AutoCompleteResource autoCompleteResource = new AutoCompleteResource(mapper, userManager, groupManager); + AutoCompleteResource autoCompleteResource = new AutoCompleteResource(mapper, userManager, groupManager, namespaceManager); dispatcher.addSingletonResource(autoCompleteResource); } @@ -269,6 +275,24 @@ public class AutoCompleteResourceTest { assertResultSize(response, defaultLimit); } + @Test + @SubjectAware(username = "user_without_autocomplete_permission", password = "secret") + public void shouldSearchNamespacesForAllUsers() throws Exception { + when(namespaceManager.getAll()).thenReturn(asList(new Namespace("hog"), new Namespace("hitchhiker"))); + + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "namespaces?q=hi") + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + assertResultSize(response, 1); + assertTrue(response.getContentAsString().contains("\"id\":\"hitchhiker\"")); + } + private User createMockUser(String id, String name) { return new User(id, name, "em@l.de"); }