implement frontend for creating tags

This commit is contained in:
Konstantin Schaper
2020-11-27 08:49:10 +01:00
parent 280f3e005e
commit 7895da0a2a
7 changed files with 203 additions and 108 deletions

View File

@@ -94,9 +94,11 @@ class Button extends React.Component<Props> {
<span className="icon is-medium"> <span className="icon is-medium">
<Icon name={icon} color="inherit" /> <Icon name={icon} color="inherit" />
</span> </span>
{(label || children) && (
<span> <span>
{label} {children} {label} {children}
</span> </span>
)}
</button> </button>
); );
} }

View File

@@ -90,6 +90,19 @@
}, },
"table": { "table": {
"tags": "Tags" "tags": "Tags"
},
"create": {
"form": {
"field": {
"name": {
"label": "Name"
}
}
},
"title": "Neuen Tag anlegen",
"hint": "Der Tag wird automatisch mit ihrem Standardschlüssel vom SCM-Manager signiert.",
"confirm": "Tag erstellen",
"cancel": "Abbrechen"
} }
}, },
"tag": { "tag": {
@@ -156,6 +169,9 @@
"buttons": { "buttons": {
"details": "Details", "details": "Details",
"sources": "Sources" "sources": "Sources"
},
"tag": {
"create": "Tag erstellen"
} }
}, },
"commit": { "commit": {

View File

@@ -90,6 +90,19 @@
}, },
"table": { "table": {
"tags": "Tags" "tags": "Tags"
},
"create": {
"form": {
"field": {
"name": {
"label": "Name"
}
}
},
"title": "Create a new tag",
"hint": "The tag will be automatically signed with your default key by the SCM-Manager.",
"confirm": "Create Tag",
"cancel": "Cancel"
} }
}, },
"tag": { "tag": {
@@ -156,6 +169,9 @@
"more": "{{count}} more", "more": "{{count}} more",
"count": "{{count}} Contributor", "count": "{{count}} Contributor",
"count_plural": "{{count}} Contributors" "count_plural": "{{count}} Contributors"
},
"tag": {
"create": "Create Tag"
} }
}, },
"commit": { "commit": {

View File

@@ -26,7 +26,7 @@ import { Trans, useTranslation, WithTranslation, withTranslation } from "react-i
import classNames from "classnames"; import classNames from "classnames";
import styled from "styled-components"; import styled from "styled-components";
import { ExtensionPoint } from "@scm-manager/ui-extensions"; import { ExtensionPoint } from "@scm-manager/ui-extensions";
import { Changeset, ParentChangeset, Repository } from "@scm-manager/ui-types"; import { Changeset, Link, ParentChangeset, Repository } from "@scm-manager/ui-types";
import { import {
AvatarImage, AvatarImage,
AvatarWrapper, AvatarWrapper,
@@ -41,7 +41,10 @@ import {
FileControlFactory, FileControlFactory,
Icon, Icon,
Level, Level,
SignatureIcon SignatureIcon,
Modal,
InputField,
apiClient
} from "@scm-manager/ui-components"; } from "@scm-manager/ui-components";
import ContributorTable from "./ContributorTable"; import ContributorTable from "./ContributorTable";
import { Link as ReactLink } from "react-router-dom"; import { Link as ReactLink } from "react-router-dom";
@@ -50,6 +53,7 @@ type Props = WithTranslation & {
changeset: Changeset; changeset: Changeset;
repository: Repository; repository: Repository;
fileControlFactory?: FileControlFactory; fileControlFactory?: FileControlFactory;
refetchChangeset?: (re) => void;
}; };
type State = { type State = {
@@ -82,11 +86,12 @@ const ContributorLine = styled.div`
`; `;
const ContributorColumn = styled.p` const ContributorColumn = styled.p`
flex-grow: 1; flex-grow: 0;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
min-width: 0; min-width: 0;
margin-right: 16px;
`; `;
const CountColumn = styled.p` const CountColumn = styled.p`
@@ -159,17 +164,10 @@ const Contributors: FC<{ changeset: Changeset }> = ({ changeset }) => {
); );
}; };
class ChangesetDetails extends React.Component<Props, State> { const ChangesetDetails: FC<Props> = ({ changeset, repository, fileControlFactory, t , refetchChangeset}) => {
constructor(props: Props) { const [collapsed, setCollapsed] = useState(false);
super(props); const [isTagCreationModalVisible, setTagCreationModalVisible] = useState(false);
this.state = { const [newTagName, setNewTagName] = useState("");
collapsed: false
};
}
render() {
const { changeset, repository, fileControlFactory, t } = this.props;
const { collapsed } = this.state;
const description = changesets.parseDescription(changeset.description); const description = changesets.parseDescription(changeset.description);
const id = <ChangesetId repository={repository} changeset={changeset} link={false} />; const id = <ChangesetId repository={repository} changeset={changeset} link={false} />;
@@ -179,6 +177,28 @@ class ChangesetDetails extends React.Component<Props, State> {
{parent.id.substring(0, 7)} {parent.id.substring(0, 7)}
</ReactLink> </ReactLink>
)); ));
const showCreateButton = "tag" in changeset._links;
const collapseDiffs = () => {
setCollapsed(!collapsed);
};
const createTag = () => {
apiClient
.post((changeset._links["tag"] as Link).href, {
revision: changeset.id,
name: newTagName
})
.then(() => {
refetchChangeset?.();
closeTagCreationModal();
});
};
const closeTagCreationModal = () => {
setNewTagName("");
setTagCreationModalVisible(false);
};
return ( return (
<> <>
@@ -218,6 +238,43 @@ class ChangesetDetails extends React.Component<Props, State> {
<div className="media-right"> <div className="media-right">
<ChangesetTags changeset={changeset} /> <ChangesetTags changeset={changeset} />
</div> </div>
<div className="media-right">
{showCreateButton && (
<Button
color="success"
className="tag"
label={(changeset._embedded["tags"]?.length <= 1 && t("changeset.tag.create")) || ""}
icon="plus"
action={() => setTagCreationModalVisible(true)}
/>
)}
{isTagCreationModalVisible && (
<Modal
title={t("tags.create.title")}
active={true}
body={
<>
<InputField
name="name"
label={t("tags.create.form.field.name.label")}
onChange={val => setNewTagName(val)}
value={newTagName}
/>
<div>{t("tags.create.hint")}</div>
</>
}
footer={
<>
<Button action={() => closeTagCreationModal()}>{t("tags.create.cancel")}</Button>
<Button color="success" action={() => createTag()}>
{t("tags.create.confirm")}
</Button>
</>
}
closeFunction={() => closeTagCreationModal()}
/>
)}
</div>
</article> </article>
<p> <p>
{description.message.split("\n").map((item, key) => { {description.message.split("\n").map((item, key) => {
@@ -243,7 +300,7 @@ class ChangesetDetails extends React.Component<Props, State> {
<BottomMarginLevel <BottomMarginLevel
right={ right={
<Button <Button
action={this.collapseDiffs} action={collapseDiffs}
color="default" color="default"
icon={collapsed ? "eye" : "eye-slash"} icon={collapsed ? "eye" : "eye-slash"}
label={t("changesets.collapseDiffs")} label={t("changesets.collapseDiffs")}
@@ -255,13 +312,6 @@ class ChangesetDetails extends React.Component<Props, State> {
</div> </div>
</> </>
); );
}
collapseDiffs = () => {
this.setState(state => ({
collapsed: !state.collapsed
}));
}; };
}
export default withTranslation("repos")(ChangesetDetails); export default withTranslation("repos")(ChangesetDetails);

View File

@@ -29,6 +29,7 @@ import { WithTranslation, withTranslation } from "react-i18next";
import { Changeset, Repository } from "@scm-manager/ui-types"; import { Changeset, Repository } from "@scm-manager/ui-types";
import { ErrorPage, Loading } from "@scm-manager/ui-components"; import { ErrorPage, Loading } from "@scm-manager/ui-components";
import { import {
fetchChangeset,
fetchChangesetIfNeeded, fetchChangesetIfNeeded,
getChangeset, getChangeset,
getFetchChangesetFailure, getFetchChangesetFailure,
@@ -45,6 +46,7 @@ type Props = WithTranslation & {
loading: boolean; loading: boolean;
error: Error; error: Error;
fetchChangesetIfNeeded: (repository: Repository, id: string) => void; fetchChangesetIfNeeded: (repository: Repository, id: string) => void;
refetchChangeset: (repository: Repository, id: string) => void;
match: any; match: any;
}; };
@@ -62,7 +64,7 @@ class ChangesetView extends React.Component<Props> {
} }
render() { render() {
const { changeset, loading, error, t, repository, fileControlFactoryFactory } = this.props; const { changeset, loading, error, t, repository, fileControlFactoryFactory, refetchChangeset } = this.props;
if (error) { if (error) {
return <ErrorPage title={t("changesets.errorTitle")} subtitle={t("changesets.errorSubtitle")} error={error} />; return <ErrorPage title={t("changesets.errorTitle")} subtitle={t("changesets.errorSubtitle")} error={error} />;
@@ -75,6 +77,7 @@ class ChangesetView extends React.Component<Props> {
changeset={changeset} changeset={changeset}
repository={repository} repository={repository}
fileControlFactory={fileControlFactoryFactory && fileControlFactoryFactory(changeset)} fileControlFactory={fileControlFactoryFactory && fileControlFactoryFactory(changeset)}
refetchChangeset={() => refetchChangeset(repository, changeset.id)}
/> />
); );
} }
@@ -98,6 +101,9 @@ const mapDispatchToProps = (dispatch: any) => {
return { return {
fetchChangesetIfNeeded: (repository: Repository, id: string) => { fetchChangesetIfNeeded: (repository: Repository, id: string) => {
dispatch(fetchChangesetIfNeeded(repository, id)); dispatch(fetchChangesetIfNeeded(repository, id));
},
refetchChangeset: (repository: Repository, id: string) => {
dispatch(fetchChangeset(repository, id));
} }
}; };
}; };

View File

@@ -130,6 +130,7 @@ public abstract class DefaultChangesetToChangesetDtoMapper extends HalAppenderMa
embeddedBuilder.with("tags", tagCollectionToDtoMapper.getTagDtoList(namespace, name, embeddedBuilder.with("tags", tagCollectionToDtoMapper.getTagDtoList(namespace, name,
getListOfObjects(source.getTags(), tags::getTagByName))); getListOfObjects(source.getTags(), tags::getTagByName)));
} }
linksBuilder.single(link("tag", resourceLinks.tag().create(namespace, name)));
} }
if (repositoryService.isSupported(Command.BRANCHES)) { if (repositoryService.isSupported(Command.BRANCHES)) {
embeddedBuilder.with("branches", branchCollectionToDtoMapper.getBranchDtoList(repository, embeddedBuilder.with("branches", branchCollectionToDtoMapper.getBranchDtoList(repository,

View File

@@ -449,6 +449,10 @@ class ResourceLinks {
return tagLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("tags").parameters().method("delete").parameters(tagName).href(); return tagLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("tags").parameters().method("delete").parameters(tagName).href();
} }
String create(String namespace, String name) {
return tagLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("tags").parameters().method("create").parameters().href();
}
String all(String namespace, String name) { String all(String namespace, String name) {
return tagLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("tags").parameters().method("getAll").parameters().href(); return tagLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("tags").parameters().method("getAll").parameters().href();
} }