diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c9f43e6ae..7f838a5766 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Added +- Rename repository name (and namespace if permitted) ([#1218](https://github.com/scm-manager/scm-manager/pull/1218)) - enrich commit mentions in markdown viewer by internal links ([#1210](https://github.com/scm-manager/scm-manager/pull/1210)) - restart service after rpm or deb package upgrade diff --git a/scm-core/src/main/java/sonia/scm/repository/ChangeNamespaceNotAllowedException.java b/scm-core/src/main/java/sonia/scm/repository/ChangeNamespaceNotAllowedException.java new file mode 100644 index 0000000000..a9d4f8a631 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/ChangeNamespaceNotAllowedException.java @@ -0,0 +1,43 @@ +/* + * 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. + */ + +package sonia.scm.repository; + +import sonia.scm.BadRequestException; +import sonia.scm.ContextEntry; + +@SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here +public class ChangeNamespaceNotAllowedException extends BadRequestException { + + public ChangeNamespaceNotAllowedException(Repository repository) { + super(ContextEntry.ContextBuilder.entity(repository).build(), "change of namespace is not allowed in current namespace strategy"); + } + + private static final String CODE = "ERS2vYb7U1"; + + @Override + public String getCode() { + return CODE; + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/NamespaceStrategy.java b/scm-core/src/main/java/sonia/scm/repository/NamespaceStrategy.java index 4cb7cc32d9..4548d2b82b 100644 --- a/scm-core/src/main/java/sonia/scm/repository/NamespaceStrategy.java +++ b/scm-core/src/main/java/sonia/scm/repository/NamespaceStrategy.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.repository; import sonia.scm.plugin.ExtensionPoint; @@ -36,8 +36,16 @@ public interface NamespaceStrategy { * Create new namespace for the given repository. * * @param repository repository - * * @return namespace */ String createNamespace(Repository repository); + + /** + * Checks if the namespace can be changed when using this namespace strategy + * + * @return namespace can be changed + */ + default boolean canBeChanged() { + return false; + } } diff --git a/scm-core/src/main/java/sonia/scm/repository/Repository.java b/scm-core/src/main/java/sonia/scm/repository/Repository.java index b1de749cdf..bd02021ed6 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Repository.java +++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java @@ -53,7 +53,7 @@ import java.util.Set; */ @StaticPermissions( value = "repository", - permissions = {"read", "modify", "delete", "healthCheck", "pull", "push", "permissionRead", "permissionWrite"}, + permissions = {"read", "modify", "delete", "rename", "healthCheck", "pull", "push", "permissionRead", "permissionWrite"}, custom = true, customGlobal = true ) @XmlAccessorType(XmlAccessType.FIELD) diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java index b05a8fd01f..7269b3517f 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.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.repository; //~--- non-JDK imports -------------------------------------------------------- @@ -38,18 +38,15 @@ import java.util.Collection; * This class is a singleton and is available via injection. * * @author Sebastian Sdorra - * * @apiviz.uses sonia.scm.repository.RepositoryHandler */ public interface RepositoryManager - extends TypeManager -{ + extends TypeManager { /** * Fire {@link RepositoryHookEvent} to the event bus. * * @param event hook event - * * @since 2.0.0 */ public void fireHookEvent(RepositoryHookEvent event); @@ -58,9 +55,7 @@ public interface RepositoryManager * Imports an existing {@link Repository}. * Note: This method should only be called from a {@link RepositoryHandler}. * - * * @param repository {@link Repository} to import - * * @throws IOException */ public void importRepository(Repository repository) throws IOException; @@ -71,10 +66,7 @@ public interface RepositoryManager * Returns a {@link Repository} by its namespace and name or * null if the {@link Repository} could not be found. * - * * @param namespaceAndName namespace and name of the {@link Repository} - * - * * @return {@link Repository} by its namespace and name or null * if the {@link Repository} could not be found */ @@ -83,7 +75,6 @@ public interface RepositoryManager /** * Returns all configured repository types. * - * * @return all configured repository types */ public Collection getConfiguredTypes(); @@ -91,11 +82,17 @@ public interface RepositoryManager /** * Returns a {@link RepositoryHandler} by the given type (hg, git, svn ...). * - * * @param type the type of the {@link RepositoryHandler} - * * @return {@link RepositoryHandler} by the given type */ @Override public RepositoryHandler getHandler(String type); + + /** + * @param repository the repository {@link Repository} + * @param newNameSpace the new repository namespace + * @param newName the new repository name + * @return {@link Repository} the renamed repository + */ + public Repository rename(Repository repository, String newNameSpace, String newName); } diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java index fa45106ab9..fdb8dfc1a8 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.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.repository; //~--- non-JDK imports -------------------------------------------------------- @@ -42,17 +42,14 @@ import java.util.Collection; */ public class RepositoryManagerDecorator extends ManagerDecorator - implements RepositoryManager -{ + implements RepositoryManager { /** * Constructs ... * - * * @param decorated */ - public RepositoryManagerDecorator(RepositoryManager decorated) - { + public RepositoryManagerDecorator(RepositoryManager decorated) { super(decorated); this.decorated = decorated; } @@ -63,8 +60,7 @@ public class RepositoryManagerDecorator * {@inheritDoc} */ @Override - public void fireHookEvent(RepositoryHookEvent event) - { + public void fireHookEvent(RepositoryHookEvent event) { decorated.fireHookEvent(event); } @@ -79,65 +75,66 @@ public class RepositoryManagerDecorator //~--- get methods ---------------------------------------------------------- @Override - public Repository get(NamespaceAndName namespaceAndName) - { + public Repository get(NamespaceAndName namespaceAndName) { return decorated.get(namespaceAndName); } /** * {@inheritDoc} * - * * @return */ @Override - public Collection getConfiguredTypes() - { + public Collection getConfiguredTypes() { return decorated.getConfiguredTypes(); } /** * Returns the decorated {@link RepositoryManager}. * - * * @return decorated {@link RepositoryManager} - * * @since 1.34 */ - public RepositoryManager getDecorated() - { + public RepositoryManager getDecorated() { return decorated; } /** * {@inheritDoc} * - * * @param type - * * @return */ @Override @SuppressWarnings("unchecked") - public RepositoryHandler getHandler(String type) - { + public RepositoryHandler getHandler(String type) { return decorated.getHandler(type); } /** * {@inheritDoc} * + * @return + */ + @Override + public Collection getTypes() { + return decorated.getTypes(); + } + + /** + * {@inheritDoc} * * @return */ @Override - public Collection getTypes() - { - return decorated.getTypes(); + public Repository rename(Repository repository, String newNamespace, String newName) { + return decorated.rename(repository, newNamespace, newName); } //~--- fields --------------------------------------------------------------- - /** Field description */ + /** + * Field description + */ private final RepositoryManager decorated; } diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryModificationEvent.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryModificationEvent.java index c61bbd2f03..8b199622d4 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryModificationEvent.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryModificationEvent.java @@ -49,7 +49,7 @@ public final class RepositoryModificationEvent extends RepositoryEvent implement */ public RepositoryModificationEvent(HandlerEventType eventType, Repository item, Repository itemBeforeModification) { - super(eventType, item); + super(eventType, item, itemBeforeModification); this.itemBeforeModification = itemBeforeModification; } diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index 808bf431e9..e5c16389ea 100644 --- a/scm-ui/ui-webapp/public/locales/de/repos.json +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -113,7 +113,8 @@ "repositoryForm": { "subtitle": "Repository bearbeiten", "submit": "Speichern", - "initializeRepository": "Repository initiieren" + "initializeRepository": "Repository initiieren", + "dangerZone": "Gefahrenzone" }, "sources": { "file-tree": { @@ -196,6 +197,8 @@ }, "deleteRepo": { "button": "Repository löschen", + "subtitle": "Löscht dieses Repository", + "description": "Diese Aktion kann nicht rückgangig gemacht werden.", "confirmAlert": { "title": "Repository löschen", "message": "Soll das Repository wirklich gelöscht werden?", @@ -203,6 +206,22 @@ "cancel": "Nein" } }, + "renameRepo": { + "button": "Repository umbenennen", + "subtitle": "Benennt dieses Repository um", + "description": "Es werden keine Weiterleitung auf den neuen Namen eingerichtet.", + "modal": { + "title": "Repository umbenennen", + "label": { + "repoName": "Repository Name", + "repoNamespace": "Repository Namespace" + }, + "button": { + "rename": "Umbenennen", + "cancel": "Abbrechen" + } + } + }, "diff": { "sideBySide": "Zur zweispaltigen Ansicht wechseln", "combined": "Zur kombinierten Ansicht wechseln", diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index e98052aba3..09d54ff58a 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -113,7 +113,8 @@ "repositoryForm": { "subtitle": "Edit Repository", "submit": "Save", - "initializeRepository": "Initialize repository" + "initializeRepository": "Initialize repository", + "dangerZone": "Danger Zone" }, "sources": { "file-tree": { @@ -196,6 +197,8 @@ }, "deleteRepo": { "button": "Delete Repository", + "subtitle": "Deletes this repository", + "description": "Once a repository was deleted, this cannot be undone. Please be careful with this action.", "confirmAlert": { "title": "Delete repository", "message": "Do you really want to delete the repository?", @@ -203,6 +206,22 @@ "cancel": "No" } }, + "renameRepo": { + "button": "Rename Repository", + "subtitle": "Renames this repository", + "description": "There will be no redirects to the renamed repository.", + "modal": { + "title": "Rename repository", + "label": { + "repoName": "Repository name", + "repoNamespace": "Repository namespace" + }, + "button": { + "rename": "Rename", + "cancel": "Cancel" + } + } + }, "diff": { "changes": { "add": "added", diff --git a/scm-ui/ui-webapp/src/admin/containers/Admin.tsx b/scm-ui/ui-webapp/src/admin/containers/Admin.tsx index 089cb959fb..51308f5af1 100644 --- a/scm-ui/ui-webapp/src/admin/containers/Admin.tsx +++ b/scm-ui/ui-webapp/src/admin/containers/Admin.tsx @@ -44,7 +44,7 @@ import GlobalConfig from "./GlobalConfig"; import RepositoryRoles from "../roles/containers/RepositoryRoles"; import SingleRepositoryRole from "../roles/containers/SingleRepositoryRole"; import CreateRepositoryRole from "../roles/containers/CreateRepositoryRole"; -import { StateMenuContextProvider } from "@scm-manager/ui-components/src/navigation/MenuContext"; +import { StateMenuContextProvider } from "@scm-manager/ui-components"; type Props = RouteComponentProps & WithTranslation & { diff --git a/scm-ui/ui-webapp/src/admin/plugins/containers/PluginsOverview.tsx b/scm-ui/ui-webapp/src/admin/plugins/containers/PluginsOverview.tsx index 74a89d3828..7605495810 100644 --- a/scm-ui/ui-webapp/src/admin/plugins/containers/PluginsOverview.tsx +++ b/scm-ui/ui-webapp/src/admin/plugins/containers/PluginsOverview.tsx @@ -54,7 +54,7 @@ import PluginBottomActions from "../components/PluginBottomActions"; import ExecutePendingActionModal from "../components/ExecutePendingActionModal"; import CancelPendingActionModal from "../components/CancelPendingActionModal"; import UpdateAllActionModal from "../components/UpdateAllActionModal"; -import { Plugin } from "@scm-manager/ui-types/src"; +import { Plugin } from "@scm-manager/ui-types"; import ShowPendingModal from "../components/ShowPendingModal"; type Props = WithTranslation & { diff --git a/scm-ui/ui-webapp/src/groups/containers/CreateGroup.tsx b/scm-ui/ui-webapp/src/groups/containers/CreateGroup.tsx index 0709681199..ceeeae20f8 100644 --- a/scm-ui/ui-webapp/src/groups/containers/CreateGroup.tsx +++ b/scm-ui/ui-webapp/src/groups/containers/CreateGroup.tsx @@ -31,7 +31,7 @@ import { Page } from "@scm-manager/ui-components"; import { getGroupsLink, getUserAutoCompleteLink } from "../../modules/indexResource"; import { createGroup, createGroupReset, getCreateGroupFailure, isCreateGroupPending } from "../modules/groups"; import GroupForm from "../components/GroupForm"; -import { apiClient } from "@scm-manager/ui-components/src"; +import { apiClient } from "@scm-manager/ui-components"; type Props = WithTranslation & { createGroup: (link: string, group: Group, callback?: () => void) => void; diff --git a/scm-ui/ui-webapp/src/groups/containers/EditGroup.tsx b/scm-ui/ui-webapp/src/groups/containers/EditGroup.tsx index df3991cfd0..d1471b59e3 100644 --- a/scm-ui/ui-webapp/src/groups/containers/EditGroup.tsx +++ b/scm-ui/ui-webapp/src/groups/containers/EditGroup.tsx @@ -31,7 +31,7 @@ import { DisplayedUser, Group } from "@scm-manager/ui-types"; import { ErrorNotification } from "@scm-manager/ui-components"; import { getUserAutoCompleteLink } from "../../modules/indexResource"; import DeleteGroup from "./DeleteGroup"; -import { apiClient } from "@scm-manager/ui-components/src"; +import { apiClient } from "@scm-manager/ui-components"; import { compose } from "redux"; type Props = { diff --git a/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx b/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx index 07e0b276b7..e21ea133bc 100644 --- a/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx +++ b/scm-ui/ui-webapp/src/repos/components/form/RepositoryForm.tsx @@ -26,8 +26,9 @@ import styled from "styled-components"; import { WithTranslation, withTranslation } from "react-i18next"; import { ExtensionPoint } from "@scm-manager/ui-extensions"; import { Repository, RepositoryType } from "@scm-manager/ui-types"; -import { Checkbox, Level, InputField, Select, SubmitButton, Subtitle, Textarea } from "@scm-manager/ui-components"; +import { Checkbox, InputField, Level, Select, SubmitButton, Subtitle, Textarea } from "@scm-manager/ui-components"; import * as validator from "./repositoryValidation"; +import { CUSTOM_NAMESPACE_STRATEGY } from "../../modules/repos"; const CheckboxWrapper = styled.div` margin-top: 2em; @@ -59,8 +60,6 @@ type State = { contactValidationError: boolean; }; -const CUSTOM_NAMESPACE_STRATEGY = "CustomNamespaceStrategy"; - class RepositoryForm extends React.Component { constructor(props: Props) { super(props); @@ -108,7 +107,7 @@ class RepositoryForm extends React.Component { ); }; - submit = (event: Event) => { + submit = (event: React.FormEvent) => { event.preventDefault(); if (this.isValid()) { this.props.submitForm(this.state.repository, this.state.initRepository); diff --git a/scm-ui/ui-webapp/src/repos/containers/DangerZone.tsx b/scm-ui/ui-webapp/src/repos/containers/DangerZone.tsx new file mode 100644 index 0000000000..5fb14e4557 --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/containers/DangerZone.tsx @@ -0,0 +1,73 @@ +/* + * 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 React, { FC } from "react"; +import { Repository, Links } from "@scm-manager/ui-types"; +import RenameRepository from "./RenameRepository"; +import DeleteRepo from "./DeleteRepo"; +import styled from "styled-components"; +import { Subtitle } from "@scm-manager/ui-components"; +import { useTranslation } from "react-i18next"; + +type Props = { + repository: Repository; + indexLinks: Links; +}; + +const DangerZoneContainer = styled.div` + padding: 1rem; + border: 1px solid #ff6a88; + border-radius: 5px; + > *:not(:last-child) { + padding-bottom: 1.5rem; + border-bottom: solid 2px whitesmoke; + } +`; + +const DangerZone: FC = ({ repository, indexLinks }) => { + const [t] = useTranslation("repos"); + + const dangerZone = []; + if (repository?._links?.rename || repository?._links?.renameWithNamespace) { + dangerZone.push(); + } + if (repository?._links?.delete) { + // @ts-ignore + dangerZone.push(); + } + + if (dangerZone.length === 0) { + return null; + } + + return ( + <> +
+ + {dangerZone.map(entry => entry)} + + ); +}; + +export default DangerZone; diff --git a/scm-ui/ui-webapp/src/repos/containers/DeleteRepo.tsx b/scm-ui/ui-webapp/src/repos/containers/DeleteRepo.tsx index 4b782fbcaf..8585e98466 100644 --- a/scm-ui/ui-webapp/src/repos/containers/DeleteRepo.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/DeleteRepo.tsx @@ -24,23 +24,21 @@ import React from "react"; import { connect } from "react-redux"; import { compose } from "redux"; -import { withRouter } from "react-router-dom"; +import { RouteComponentProps, withRouter } from "react-router-dom"; import { WithTranslation, withTranslation } from "react-i18next"; import { History } from "history"; import { Repository } from "@scm-manager/ui-types"; -import { confirmAlert, DeleteButton, ErrorNotification, Level } from "@scm-manager/ui-components"; +import { confirmAlert, DeleteButton, ErrorNotification, Level, ButtonGroup } from "@scm-manager/ui-components"; import { deleteRepo, getDeleteRepoFailure, isDeleteRepoPending } from "../modules/repos"; -type Props = WithTranslation & { - loading: boolean; - error: Error; - repository: Repository; - confirmDialog?: boolean; - deleteRepo: (p1: Repository, p2: () => void) => void; - - // context props - history: History; -}; +type Props = RouteComponentProps & + WithTranslation & { + loading: boolean; + error: Error; + repository: Repository; + confirmDialog?: boolean; + deleteRepo: (p1: Repository, p2: () => void) => void; + }; class DeleteRepo extends React.Component { static defaultProps = { @@ -88,9 +86,16 @@ class DeleteRepo extends React.Component { return ( <> -
- } /> + + {t("deleteRepo.subtitle")} +

{t("deleteRepo.description")}

+ + } + right={} + /> ); } diff --git a/scm-ui/ui-webapp/src/repos/containers/EditRepo.tsx b/scm-ui/ui-webapp/src/repos/containers/EditRepo.tsx index a6e9874628..e4d16dcfa8 100644 --- a/scm-ui/ui-webapp/src/repos/containers/EditRepo.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/EditRepo.tsx @@ -25,17 +25,19 @@ import React from "react"; import { connect } from "react-redux"; import { withRouter } from "react-router-dom"; import RepositoryForm from "../components/form"; -import DeleteRepo from "./DeleteRepo"; -import { Repository } from "@scm-manager/ui-types"; +import { Repository, Links } from "@scm-manager/ui-types"; import { getModifyRepoFailure, isModifyRepoPending, modifyRepo, modifyRepoReset } from "../modules/repos"; import { History } from "history"; import { ErrorNotification } from "@scm-manager/ui-components"; import { ExtensionPoint } from "@scm-manager/ui-extensions"; import { compose } from "redux"; +import DangerZone from "./DangerZone"; +import { getLinks } from "../../modules/indexResource"; type Props = { loading: boolean; error: Error; + indexLinks: Links; modifyRepo: (p1: Repository, p2: () => void) => void; modifyRepoReset: (p: Repository) => void; @@ -69,7 +71,7 @@ class EditRepo extends React.Component { }; render() { - const { loading, error, repository } = this.props; + const { loading, error, repository, indexLinks } = this.props; const url = this.matchedUrl(); @@ -79,7 +81,7 @@ class EditRepo extends React.Component { }; return ( -
+ <> { }} /> - -
+ + ); } } @@ -99,9 +101,12 @@ const mapStateToProps = (state: any, ownProps: Props) => { const { namespace, name } = ownProps.repository; const loading = isModifyRepoPending(state, namespace, name); const error = getModifyRepoFailure(state, namespace, name); + const indexLinks = getLinks(state); + return { loading, - error + error, + indexLinks }; }; diff --git a/scm-ui/ui-webapp/src/repos/containers/RenameRepository.tsx b/scm-ui/ui-webapp/src/repos/containers/RenameRepository.tsx new file mode 100644 index 0000000000..ede6414029 --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/containers/RenameRepository.tsx @@ -0,0 +1,178 @@ +/* + * 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 React, { FC, useEffect, useState } from "react"; +import { Link, Links, Repository } from "@scm-manager/ui-types"; +import { CONTENT_TYPE, CUSTOM_NAMESPACE_STRATEGY } from "../modules/repos"; +import { Button, ButtonGroup, ErrorNotification, InputField, Level, Loading, Modal } from "@scm-manager/ui-components"; +import { useTranslation } from "react-i18next"; +import { apiClient } from "@scm-manager/ui-components"; +import { useHistory } from "react-router-dom"; +import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import * as validator from "../components/form/repositoryValidation"; + +type Props = { + repository: Repository; + indexLinks: Links; +}; + +const RenameRepository: FC = ({ repository, indexLinks }) => { + let history = useHistory(); + const [t] = useTranslation("repos"); + const [error, setError] = useState(undefined); + const [loading, setLoading] = useState(false); + const [showModal, setShowModal] = useState(false); + const [name, setName] = useState(repository.name); + const [namespace, setNamespace] = useState(repository.namespace); + const [nameValidationError, setNameValidationError] = useState(false); + const [namespaceValidationError, setNamespaceValidationError] = useState(false); + const [currentNamespaceStrategie, setCurrentNamespaceStrategy] = useState(""); + + useEffect(() => { + apiClient + .get((indexLinks?.namespaceStrategies as Link).href) + .then(result => result.json()) + .then(result => setCurrentNamespaceStrategy(result.current)) + .catch(setError); + }, [repository]); + + if (error) { + return ; + } + + if (loading) { + return ; + } + + const isValid = + !nameValidationError && + !namespaceValidationError && + (repository.name !== name || repository.namespace !== namespace); + + const handleNamespaceChange = (namespace: string) => { + setNamespaceValidationError(!validator.isNameValid(namespace)); + setNamespace(namespace); + }; + + const handleNameChange = (name: string) => { + setNameValidationError(!validator.isNameValid(name)); + 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 (currentNamespaceStrategie === CUSTOM_NAMESPACE_STRATEGY) { + return ; + } + + return ; + }; + + const rename = () => { + setLoading(true); + const url = repository?._links?.renameWithNamespace + ? (repository?._links?.renameWithNamespace as Link).href + : (repository?._links?.rename as Link).href; + + apiClient + .post(url, { name, namespace }, CONTENT_TYPE) + .then(() => setLoading(false)) + .then(() => history.push(`/repo/${namespace}/${name}`)) + .catch(setError); + }; + + const modalBody = ( +
+ + {renderNamespaceField()} +
+ ); + + const footer = ( + <> + +