Add files to empty repository (#1717)

It should also be possible to create new files in empty non-initiated repositories with the help of scm-manager/scm-editor-plugin/pull/39. So that the plugin can mount itself, a new endpoint was provided hereby.

Co-authored-by: Eduard Heimbuch <eduard.heimbuch@cloudogu.com>
This commit is contained in:
Florian Scholdei
2021-07-13 11:40:49 +02:00
committed by GitHub
parent 1ce19eea4a
commit 0d0f9995fe
7 changed files with 168 additions and 105 deletions

View File

@@ -0,0 +1,2 @@
- type: Added
description: Create files in empty non-initiated repositories ([#1717](https://github.com/scm-manager/scm-manager/pull/1717))

View File

@@ -26,12 +26,12 @@ import { useTranslation } from "react-i18next";
import { useHistory, useLocation, Link } from "react-router-dom"; import { useHistory, useLocation, Link } from "react-router-dom";
import classNames from "classnames"; import classNames from "classnames";
import styled from "styled-components"; import styled from "styled-components";
import { binder, ExtensionPoint } from "@scm-manager/ui-extensions"; import { urls } from "@scm-manager/ui-api";
import { Branch, Repository, File } from "@scm-manager/ui-types"; import { Branch, Repository, File } from "@scm-manager/ui-types";
import { binder, ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions";
import Icon from "./Icon"; import Icon from "./Icon";
import Tooltip from "./Tooltip"; import Tooltip from "./Tooltip";
import copyToClipboard from "./CopyToClipboard"; import copyToClipboard from "./CopyToClipboard";
import { urls } from "@scm-manager/ui-api";
type Props = { type Props = {
repository: Repository; repository: Repository;
@@ -65,8 +65,6 @@ const BreadcrumbNav = styled.nav`
flex: 1; flex: 1;
display: flex; display: flex;
align-items: center; align-items: center;
margin: 1rem 1rem !important;
width: 100%; width: 100%;
/* move slash to end */ /* move slash to end */
@@ -127,7 +125,7 @@ const Breadcrumb: FC<Props> = ({
baseUrl, baseUrl,
sources, sources,
permalink, permalink,
preButtons preButtons,
}) => { }) => {
const location = useLocation(); const location = useLocation();
const history = useHistory(); const history = useHistory();
@@ -138,7 +136,10 @@ const Breadcrumb: FC<Props> = ({
if (path) { if (path) {
const paths = path.split("/"); const paths = path.split("/");
return paths.map((pathFragment, index) => { return paths.map((pathFragment, index) => {
const currPath = paths.slice(0, index + 1).join("/"); let currPath = paths.slice(0, index + 1).join("/");
if (!currPath.endsWith("/")) {
currPath = currPath + "/";
}
if (paths.length - 1 === index) { if (paths.length - 1 === index) {
return ( return (
<li className="is-active" key={index}> <li className="is-active" key={index}>
@@ -172,15 +173,45 @@ const Breadcrumb: FC<Props> = ({
).finally(() => setCopying(false)); ).finally(() => setCopying(false));
}; };
let homeUrl = baseUrl + "/"; const renderBreadcrumbNav = () => {
if (revision) { let prefixButtons = null;
homeUrl += encodeURIComponent(revision) + "/"; if (preButtons) {
} prefixButtons = <PrefixButton>{preButtons}</PrefixButton>;
}
let prefixButtons = null; let homeUrl = baseUrl + "/";
if (preButtons) { if (revision) {
prefixButtons = <PrefixButton>{preButtons}</PrefixButton>; homeUrl += encodeURIComponent(revision) + "/";
} } else {
homeUrl = `/repo/${repository.namespace}/${repository.name}/code/sources/`;
}
return (
<BreadcrumbNav
className={classNames("breadcrumb", "sources-breadcrumb", "mx-2", "my-4")}
aria-label="breadcrumbs"
>
{prefixButtons}
<ul>
<li>
<Link to={homeUrl}>
<HomeIcon title={t("breadcrumb.home")} name="home" color="inherit" />
</Link>
</li>
{pathSection()}
</ul>
<PermaLinkWrapper className="ml-1">
{copying ? (
<Icon name="spinner fa-spin" />
) : (
<Tooltip message={t("breadcrumb.copyPermalink")}>
<Icon name="link" color="inherit" onClick={() => copySource()} />
</Tooltip>
)}
</PermaLinkWrapper>
</BreadcrumbNav>
);
};
const extProps = { const extProps = {
baseUrl, baseUrl,
@@ -188,40 +219,33 @@ const Breadcrumb: FC<Props> = ({
branch: branch ? branch : defaultBranch, branch: branch ? branch : defaultBranch,
path, path,
sources, sources,
repository repository,
};
const renderExtensionPoints = () => {
if (
binder.hasExtension<extensionPoints.ReposSourcesEmptyActionbar>("repos.sources.empty.actionbar") &&
sources?._embedded?.children?.length === 0
) {
return (
<ExtensionPoint
name="repos.sources.empty.actionbar"
props={{ repository, sources }}
renderAll={true}
/>
);
}
if (binder.hasExtension<extensionPoints.ReposSourcesActionbar>("repos.sources.actionbar")) {
return <ExtensionPoint name="repos.sources.actionbar" props={extProps} renderAll={true} />;
}
return null;
}; };
return ( return (
<> <>
<div className="is-flex is-align-items-center"> <div className="is-flex is-align-items-center is-justify-content-flex-end">
<BreadcrumbNav {renderBreadcrumbNav()}
className={classNames("breadcrumb", "sources-breadcrumb", "ml-1", "mb-0")} {<ActionBar className="my-2">{renderExtensionPoints()}</ActionBar>}
aria-label="breadcrumbs"
>
{prefixButtons}
<ul>
<li>
<Link to={homeUrl}>
<HomeIcon title={t("breadcrumb.home")} name="home" color="inherit" />
</Link>
</li>
{pathSection()}
</ul>
<PermaLinkWrapper className="ml-1">
{copying ? (
<Icon name="spinner fa-spin" />
) : (
<Tooltip message={t("breadcrumb.copyPermalink")}>
<Icon name="link" color="inherit" onClick={() => copySource()} />
</Tooltip>
)}
</PermaLinkWrapper>
</BreadcrumbNav>
{binder.hasExtension("repos.sources.actionbar") && (
<ActionBar>
<ExtensionPoint name="repos.sources.actionbar" props={extProps} renderAll={true} />
</ActionBar>
)}
</div> </div>
<hr className="is-marginless" /> <hr className="is-marginless" />
</> </>

View File

@@ -1663,11 +1663,11 @@ exports[`Storyshots BreadCrumb Default 1`] = `
className="Breadcrumbstories__Wrapper-sc-1eq8sgz-0 exRctT" className="Breadcrumbstories__Wrapper-sc-1eq8sgz-0 exRctT"
> >
<div <div
className="is-flex is-align-items-center" className="is-flex is-align-items-center is-justify-content-flex-end"
> >
<nav <nav
aria-label="breadcrumbs" aria-label="breadcrumbs"
className="Breadcrumb__BreadcrumbNav-zvtb4t-1 ieUHOq breadcrumb sources-breadcrumb ml-1 mb-0" className="Breadcrumb__BreadcrumbNav-zvtb4t-1 eupowG breadcrumb sources-breadcrumb mx-2 my-4"
> >
<ul> <ul>
<li> <li>
@@ -1684,7 +1684,7 @@ exports[`Storyshots BreadCrumb Default 1`] = `
<li> <li>
<a <a
className="is-ellipsis-overflow" className="is-ellipsis-overflow"
href="/scm-manager.org/scm/repo/hitchhiker/heartOfGold/sources/1/src" href="/scm-manager.org/scm/repo/hitchhiker/heartOfGold/sources/1/src/"
onClick={[Function]} onClick={[Function]}
title="src" title="src"
> >
@@ -1694,7 +1694,7 @@ exports[`Storyshots BreadCrumb Default 1`] = `
<li> <li>
<a <a
className="is-ellipsis-overflow" className="is-ellipsis-overflow"
href="/scm-manager.org/scm/repo/hitchhiker/heartOfGold/sources/1/src/main" href="/scm-manager.org/scm/repo/hitchhiker/heartOfGold/sources/1/src/main/"
onClick={[Function]} onClick={[Function]}
title="main" title="main"
> >
@@ -1704,7 +1704,7 @@ exports[`Storyshots BreadCrumb Default 1`] = `
<li> <li>
<a <a
className="is-ellipsis-overflow" className="is-ellipsis-overflow"
href="/scm-manager.org/scm/repo/hitchhiker/heartOfGold/sources/1/src/main/java" href="/scm-manager.org/scm/repo/hitchhiker/heartOfGold/sources/1/src/main/java/"
onClick={[Function]} onClick={[Function]}
title="java" title="java"
> >
@@ -1714,7 +1714,7 @@ exports[`Storyshots BreadCrumb Default 1`] = `
<li> <li>
<a <a
className="is-ellipsis-overflow" className="is-ellipsis-overflow"
href="/scm-manager.org/scm/repo/hitchhiker/heartOfGold/sources/1/src/main/java/com" href="/scm-manager.org/scm/repo/hitchhiker/heartOfGold/sources/1/src/main/java/com/"
onClick={[Function]} onClick={[Function]}
title="com" title="com"
> >
@@ -1748,6 +1748,9 @@ exports[`Storyshots BreadCrumb Default 1`] = `
</span> </span>
</span> </span>
</nav> </nav>
<div
className="Breadcrumb__ActionBar-zvtb4t-3 izqSnl my-2"
/>
</div> </div>
<hr <hr
className="is-marginless" className="is-marginless"
@@ -1760,11 +1763,11 @@ exports[`Storyshots BreadCrumb Long path 1`] = `
className="Breadcrumbstories__Wrapper-sc-1eq8sgz-0 exRctT" className="Breadcrumbstories__Wrapper-sc-1eq8sgz-0 exRctT"
> >
<div <div
className="is-flex is-align-items-center" className="is-flex is-align-items-center is-justify-content-flex-end"
> >
<nav <nav
aria-label="breadcrumbs" aria-label="breadcrumbs"
className="Breadcrumb__BreadcrumbNav-zvtb4t-1 ieUHOq breadcrumb sources-breadcrumb ml-1 mb-0" className="Breadcrumb__BreadcrumbNav-zvtb4t-1 eupowG breadcrumb sources-breadcrumb mx-2 my-4"
> >
<ul> <ul>
<li> <li>
@@ -1781,7 +1784,7 @@ exports[`Storyshots BreadCrumb Long path 1`] = `
<li> <li>
<a <a
className="is-ellipsis-overflow" className="is-ellipsis-overflow"
href="/scm-manager.org/scm/repo/hitchhiker/heartOfGold/sources/1/dream-path" href="/scm-manager.org/scm/repo/hitchhiker/heartOfGold/sources/1/dream-path/"
onClick={[Function]} onClick={[Function]}
title="dream-path" title="dream-path"
> >
@@ -1791,7 +1794,7 @@ exports[`Storyshots BreadCrumb Long path 1`] = `
<li> <li>
<a <a
className="is-ellipsis-overflow" className="is-ellipsis-overflow"
href="/scm-manager.org/scm/repo/hitchhiker/heartOfGold/sources/1/dream-path/src" href="/scm-manager.org/scm/repo/hitchhiker/heartOfGold/sources/1/dream-path/src/"
onClick={[Function]} onClick={[Function]}
title="src" title="src"
> >
@@ -1801,7 +1804,7 @@ exports[`Storyshots BreadCrumb Long path 1`] = `
<li> <li>
<a <a
className="is-ellipsis-overflow" className="is-ellipsis-overflow"
href="/scm-manager.org/scm/repo/hitchhiker/heartOfGold/sources/1/dream-path/src/main" href="/scm-manager.org/scm/repo/hitchhiker/heartOfGold/sources/1/dream-path/src/main/"
onClick={[Function]} onClick={[Function]}
title="main" title="main"
> >
@@ -1811,7 +1814,7 @@ exports[`Storyshots BreadCrumb Long path 1`] = `
<li> <li>
<a <a
className="is-ellipsis-overflow" className="is-ellipsis-overflow"
href="/scm-manager.org/scm/repo/hitchhiker/heartOfGold/sources/1/dream-path/src/main/scm-plugins" href="/scm-manager.org/scm/repo/hitchhiker/heartOfGold/sources/1/dream-path/src/main/scm-plugins/"
onClick={[Function]} onClick={[Function]}
title="scm-plugins" title="scm-plugins"
> >
@@ -1821,7 +1824,7 @@ exports[`Storyshots BreadCrumb Long path 1`] = `
<li> <li>
<a <a
className="is-ellipsis-overflow" className="is-ellipsis-overflow"
href="/scm-manager.org/scm/repo/hitchhiker/heartOfGold/sources/1/dream-path/src/main/scm-plugins/javaUtilityHomeHousingLinkReferrer" href="/scm-manager.org/scm/repo/hitchhiker/heartOfGold/sources/1/dream-path/src/main/scm-plugins/javaUtilityHomeHousingLinkReferrer/"
onClick={[Function]} onClick={[Function]}
title="javaUtilityHomeHousingLinkReferrer" title="javaUtilityHomeHousingLinkReferrer"
> >
@@ -1831,7 +1834,7 @@ exports[`Storyshots BreadCrumb Long path 1`] = `
<li> <li>
<a <a
className="is-ellipsis-overflow" className="is-ellipsis-overflow"
href="/scm-manager.org/scm/repo/hitchhiker/heartOfGold/sources/1/dream-path/src/main/scm-plugins/javaUtilityHomeHousingLinkReferrer/sonia" href="/scm-manager.org/scm/repo/hitchhiker/heartOfGold/sources/1/dream-path/src/main/scm-plugins/javaUtilityHomeHousingLinkReferrer/sonia/"
onClick={[Function]} onClick={[Function]}
title="sonia" title="sonia"
> >
@@ -1841,7 +1844,7 @@ exports[`Storyshots BreadCrumb Long path 1`] = `
<li> <li>
<a <a
className="is-ellipsis-overflow" className="is-ellipsis-overflow"
href="/scm-manager.org/scm/repo/hitchhiker/heartOfGold/sources/1/dream-path/src/main/scm-plugins/javaUtilityHomeHousingLinkReferrer/sonia/scm" href="/scm-manager.org/scm/repo/hitchhiker/heartOfGold/sources/1/dream-path/src/main/scm-plugins/javaUtilityHomeHousingLinkReferrer/sonia/scm/"
onClick={[Function]} onClick={[Function]}
title="scm" title="scm"
> >
@@ -1851,7 +1854,7 @@ exports[`Storyshots BreadCrumb Long path 1`] = `
<li> <li>
<a <a
className="is-ellipsis-overflow" className="is-ellipsis-overflow"
href="/scm-manager.org/scm/repo/hitchhiker/heartOfGold/sources/1/dream-path/src/main/scm-plugins/javaUtilityHomeHousingLinkReferrer/sonia/scm/repositoryUndergroundSupportManager" href="/scm-manager.org/scm/repo/hitchhiker/heartOfGold/sources/1/dream-path/src/main/scm-plugins/javaUtilityHomeHousingLinkReferrer/sonia/scm/repositoryUndergroundSupportManager/"
onClick={[Function]} onClick={[Function]}
title="repositoryUndergroundSupportManager" title="repositoryUndergroundSupportManager"
> >
@@ -1861,7 +1864,7 @@ exports[`Storyshots BreadCrumb Long path 1`] = `
<li> <li>
<a <a
className="is-ellipsis-overflow" className="is-ellipsis-overflow"
href="/scm-manager.org/scm/repo/hitchhiker/heartOfGold/sources/1/dream-path/src/main/scm-plugins/javaUtilityHomeHousingLinkReferrer/sonia/scm/repositoryUndergroundSupportManager/spi" href="/scm-manager.org/scm/repo/hitchhiker/heartOfGold/sources/1/dream-path/src/main/scm-plugins/javaUtilityHomeHousingLinkReferrer/sonia/scm/repositoryUndergroundSupportManager/spi/"
onClick={[Function]} onClick={[Function]}
title="spi" title="spi"
> >
@@ -1895,6 +1898,9 @@ exports[`Storyshots BreadCrumb Long path 1`] = `
</span> </span>
</span> </span>
</nav> </nav>
<div
className="Breadcrumb__ActionBar-zvtb4t-3 izqSnl my-2"
/>
</div> </div>
<hr <hr
className="is-marginless" className="is-marginless"

View File

@@ -22,14 +22,16 @@
* SOFTWARE. * SOFTWARE.
*/ */
import { ExtensionPointDefinition } from "./binder"; import React from "react";
import { import {
Branch,
IndexResources, IndexResources,
NamespaceStrategies, NamespaceStrategies,
Repository, Repository,
RepositoryCreation, RepositoryCreation,
RepositoryTypeCollection, RepositoryTypeCollection,
} from "@scm-manager/ui-types"; } from "@scm-manager/ui-types";
import { ExtensionPointDefinition } from "./binder";
type RepositoryCreatorSubFormProps = { type RepositoryCreatorSubFormProps = {
repository: RepositoryCreation; repository: RepositoryCreation;
@@ -58,3 +60,24 @@ export type RepositoryCreatorExtension = {
export type RepositoryCreator = ExtensionPointDefinition<"repos.creator", RepositoryCreatorExtension>; export type RepositoryCreator = ExtensionPointDefinition<"repos.creator", RepositoryCreatorExtension>;
export type RepositoryFlags = ExtensionPointDefinition<"repository.flags", { repository: Repository }>; export type RepositoryFlags = ExtensionPointDefinition<"repository.flags", { repository: Repository }>;
export type ReposSourcesActionbarExtensionProps = {
baseUrl: string;
revision: string;
branch: Branch | undefined;
path: string;
sources: File;
repository: Repository;
};
export type ReposSourcesActionbarExtension = React.ComponentType<ReposSourcesActionbarExtensionProps>;
export type ReposSourcesActionbar = ExtensionPointDefinition<"repos.sources.actionbar", ReposSourcesActionbarExtension>;
export type ReposSourcesEmptyActionbarExtensionProps = {
sources: File;
repository: Repository;
};
export type ReposSourcesEmptyActionbarExtension = ReposSourcesActionbarExtension;
export type ReposSourcesEmptyActionbar = ExtensionPointDefinition<
"repos.sources.empty.actionbar",
ReposSourcesEmptyActionbarExtension
>;

View File

@@ -26,7 +26,7 @@ import { Route, useLocation } from "react-router-dom";
import Sources from "../../sources/containers/Sources"; import Sources from "../../sources/containers/Sources";
import ChangesetsRoot from "../../containers/ChangesetsRoot"; import ChangesetsRoot from "../../containers/ChangesetsRoot";
import { Branch, Repository } from "@scm-manager/ui-types"; import { Branch, Repository } from "@scm-manager/ui-types";
import { ErrorPage, Loading, Notification } from "@scm-manager/ui-components"; import { ErrorPage, Loading } from "@scm-manager/ui-components";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useBranches } from "@scm-manager/ui-api"; import { useBranches } from "@scm-manager/ui-api";
import FileSearch from "./FileSearch"; import FileSearch from "./FileSearch";
@@ -71,7 +71,7 @@ const CodeOverviewWithBranches: FC<Props> = ({ repository, baseUrl }) => {
} }
if (branches.length === 0) { if (branches.length === 0) {
return <Notification type="info">{t("code.noBranches")}</Notification>; return <Sources repository={repository} baseUrl={baseUrl} />;
} }
return <CodeRouting repository={repository} baseUrl={baseUrl} branches={branches} selectedBranch={selectedBranch} />; return <CodeRouting repository={repository} baseUrl={baseUrl} branches={branches} selectedBranch={selectedBranch} />;

View File

@@ -22,17 +22,17 @@
* SOFTWARE. * SOFTWARE.
*/ */
import React, { FC, useEffect } from "react"; import React, { FC, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useHistory, useLocation, useParams } from "react-router-dom";
import { useSources } from "@scm-manager/ui-api";
import { Branch, Repository } from "@scm-manager/ui-types"; import { Branch, Repository } from "@scm-manager/ui-types";
import { Breadcrumb, Loading, Notification } from "@scm-manager/ui-components"; import { Breadcrumb, Loading, Notification } from "@scm-manager/ui-components";
import FileTree from "../components/FileTree"; import FileTree from "../components/FileTree";
import Content from "./Content"; import Content from "./Content";
import CodeActionBar from "../../codeSection/components/CodeActionBar"; import CodeActionBar from "../../codeSection/components/CodeActionBar";
import replaceBranchWithRevision from "../ReplaceBranchWithRevision"; import replaceBranchWithRevision from "../ReplaceBranchWithRevision";
import { useSources } from "@scm-manager/ui-api";
import { useHistory, useLocation, useParams } from "react-router-dom";
import FileSearchButton from "../../codeSection/components/FileSearchButton"; import FileSearchButton from "../../codeSection/components/FileSearchButton";
import { isEmptyDirectory, isRootFile } from "../utils/files"; import { isEmptyDirectory, isRootFile } from "../utils/files";
import { useTranslation } from "react-i18next";
type Props = { type Props = {
repository: Repository; repository: Repository;
@@ -84,6 +84,10 @@ const Sources: FC<Props> = ({ repository, branches, selectedBranch, baseUrl }) =
return <Loading />; return <Loading />;
} }
if (!file) {
return null;
}
const onSelectBranch = (branch?: Branch) => { const onSelectBranch = (branch?: Branch) => {
let url; let url;
if (branch) { if (branch) {
@@ -129,18 +133,17 @@ const Sources: FC<Props> = ({ repository, branches, selectedBranch, baseUrl }) =
); );
}; };
if (file && file.directory) { const renderPanelContent = () => {
let body; if (file.directory) {
if (isRootFile(file) && isEmptyDirectory(file)) { let body;
body = ( if (isRootFile(file) && isEmptyDirectory(file)) {
<div className="panel-block"> body = (
<Notification type="info">{t("sources.noSources")}</Notification> <div className="panel-block">
</div> <Notification type="info">{t("sources.noSources")}</Notification>
); </div>
} else { );
body = ( } else {
<> body = (
{renderBreadcrumb()}
<FileTree <FileTree
directory={file} directory={file}
revision={revision || file.revision} revision={revision || file.revision}
@@ -148,40 +151,45 @@ const Sources: FC<Props> = ({ repository, branches, selectedBranch, baseUrl }) =
isFetchingNextPage={isFetchingNextPage} isFetchingNextPage={isFetchingNextPage}
fetchNextPage={fetchNextPage} fetchNextPage={fetchNextPage}
/> />
</> );
}
return (
<div className="panel">
{renderBreadcrumb()}
{body}
</div>
); );
} }
return ( return (
<> <Content
file={file}
repository={repository}
revision={revision || file.revision}
breadcrumb={renderBreadcrumb()}
error={error || undefined}
/>
);
};
const hasBranchesWhenSupporting = (repository: Repository) => {
return !repository._links.branches || (branches && branches.length !== 0);
};
return (
<>
{hasBranchesWhenSupporting(repository) && (
<CodeActionBar <CodeActionBar
selectedBranch={selectedBranch} selectedBranch={selectedBranch}
branches={branches} branches={branches}
onSelectBranch={onSelectBranch} onSelectBranch={onSelectBranch}
switchViewLink={evaluateSwitchViewLink()} switchViewLink={evaluateSwitchViewLink()}
/> />
<div className="panel">{body}</div> )}
</> {renderPanelContent()}
); </>
} else { );
return (
<>
<CodeActionBar
selectedBranch={selectedBranch}
branches={branches}
onSelectBranch={onSelectBranch}
switchViewLink={evaluateSwitchViewLink()}
/>
<Content
file={file}
repository={repository}
revision={revision || file.revision}
breadcrumb={renderBreadcrumb()}
error={error}
/>
</>
);
}
}; };
export default Sources; export default Sources;

View File

@@ -28,14 +28,14 @@ export const isRootPath = (path: string) => {
}; };
export const isRootFile = (file: File) => { export const isRootFile = (file: File) => {
if (!file.directory) { if (!file?.directory) {
return false; return false;
} }
return isRootPath(file.path); return isRootPath(file.path);
}; };
export const isEmptyDirectory = (file: File) => { export const isEmptyDirectory = (file: File) => {
if (!file.directory) { if (!file?.directory) {
return false; return false;
} }
return (file._embedded?.children?.length || 0) === 0; return (file._embedded?.children?.length || 0) === 0;