mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-11 07:55:47 +01:00
improve ux
This commit is contained in:
@@ -95,7 +95,8 @@
|
|||||||
"form": {
|
"form": {
|
||||||
"field": {
|
"field": {
|
||||||
"name": {
|
"name": {
|
||||||
"label": "Name"
|
"label": "Name",
|
||||||
|
"error": "Dieser Tag existiert bereits."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -95,7 +95,8 @@
|
|||||||
"form": {
|
"form": {
|
||||||
"field": {
|
"field": {
|
||||||
"name": {
|
"name": {
|
||||||
"label": "Name"
|
"label": "Name",
|
||||||
|
"error": "This tag already exists."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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, Link, ParentChangeset, Repository } from "@scm-manager/ui-types";
|
import {Changeset, Link, ParentChangeset, Repository, Tag} from "@scm-manager/ui-types";
|
||||||
import {
|
import {
|
||||||
AvatarImage,
|
AvatarImage,
|
||||||
AvatarWrapper,
|
AvatarWrapper,
|
||||||
@@ -42,22 +42,18 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
Level,
|
Level,
|
||||||
SignatureIcon,
|
SignatureIcon,
|
||||||
Modal,
|
Tooltip,
|
||||||
InputField,
|
ErrorNotification
|
||||||
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";
|
||||||
|
import CreateTagModal from "./CreateTagModal";
|
||||||
|
|
||||||
type Props = WithTranslation & {
|
type Props = WithTranslation & {
|
||||||
changeset: Changeset;
|
changeset: Changeset;
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
fileControlFactory?: FileControlFactory;
|
fileControlFactory?: FileControlFactory;
|
||||||
refetchChangeset?: (re) => void;
|
refetchChangeset?: () => void;
|
||||||
};
|
|
||||||
|
|
||||||
type State = {
|
|
||||||
collapsed: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const RightMarginP = styled.p`
|
const RightMarginP = styled.p`
|
||||||
@@ -91,7 +87,6 @@ const ContributorColumn = styled.p`
|
|||||||
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`
|
||||||
@@ -113,7 +108,6 @@ const ContributorToggleLine = styled.p`
|
|||||||
|
|
||||||
const ChangesetSummary = styled.div`
|
const ChangesetSummary = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const SeparatedParents = styled.div`
|
const SeparatedParents = styled.div`
|
||||||
@@ -152,7 +146,7 @@ const Contributors: FC<{ changeset: Changeset }> = ({ changeset }) => {
|
|||||||
<Icon name="angle-right" /> <ChangesetAuthor changeset={changeset} />
|
<Icon name="angle-right" /> <ChangesetAuthor changeset={changeset} />
|
||||||
</ContributorColumn>
|
</ContributorColumn>
|
||||||
{signatureIcon}
|
{signatureIcon}
|
||||||
<CountColumn className={"is-hidden-mobile"}>
|
<CountColumn className={"is-hidden-mobile is-hidden-tablet-only is-hidden-desktop-only"}>
|
||||||
(
|
(
|
||||||
<span className="has-text-link">
|
<span className="has-text-link">
|
||||||
{t("changeset.contributors.count", { count: countContributors(changeset) })}
|
{t("changeset.contributors.count", { count: countContributors(changeset) })}
|
||||||
@@ -164,10 +158,10 @@ const Contributors: FC<{ changeset: Changeset }> = ({ changeset }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ChangesetDetails: FC<Props> = ({ changeset, repository, fileControlFactory, t , refetchChangeset}) => {
|
const ChangesetDetails: FC<Props> = ({ changeset, repository, fileControlFactory, t, refetchChangeset }) => {
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
const [isTagCreationModalVisible, setTagCreationModalVisible] = useState(false);
|
const [isTagCreationModalVisible, setTagCreationModalVisible] = useState(false);
|
||||||
const [newTagName, setNewTagName] = useState("");
|
const [error, setError] = useState<Error | undefined>();
|
||||||
|
|
||||||
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} />;
|
||||||
@@ -183,22 +177,9 @@ const ChangesetDetails: FC<Props> = ({ changeset, repository, fileControlFactory
|
|||||||
setCollapsed(!collapsed);
|
setCollapsed(!collapsed);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createTag = () => {
|
if (error) {
|
||||||
apiClient
|
return <ErrorNotification error={error} />;
|
||||||
.post((changeset._links["tag"] as Link).href, {
|
}
|
||||||
revision: changeset.id,
|
|
||||||
name: newTagName
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
refetchChangeset?.();
|
|
||||||
closeTagCreationModal();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeTagCreationModal = () => {
|
|
||||||
setNewTagName("");
|
|
||||||
setTagCreationModalVisible(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -238,8 +219,10 @@ const ChangesetDetails: FC<Props> = ({ changeset, repository, fileControlFactory
|
|||||||
<div className="media-right">
|
<div className="media-right">
|
||||||
<ChangesetTags changeset={changeset} />
|
<ChangesetTags changeset={changeset} />
|
||||||
</div>
|
</div>
|
||||||
<div className="media-right">
|
|
||||||
{showCreateButton && (
|
{showCreateButton && (
|
||||||
|
<div className="media-right">
|
||||||
|
<Tooltip message={t("changeset.tag.create")} location="top">
|
||||||
<Button
|
<Button
|
||||||
color="success"
|
color="success"
|
||||||
className="tag"
|
className="tag"
|
||||||
@@ -247,34 +230,22 @@ const ChangesetDetails: FC<Props> = ({ changeset, repository, fileControlFactory
|
|||||||
icon="plus"
|
icon="plus"
|
||||||
action={() => setTagCreationModalVisible(true)}
|
action={() => setTagCreationModalVisible(true)}
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{isTagCreationModalVisible && (
|
{isTagCreationModalVisible && (
|
||||||
<Modal
|
<CreateTagModal
|
||||||
title={t("tags.create.title")}
|
revision={changeset.id}
|
||||||
active={true}
|
tagNames={changeset._embedded["tags"].map((tag: Tag) => tag.name)}
|
||||||
body={
|
onClose={() => setTagCreationModalVisible(false)}
|
||||||
<>
|
onCreated={() => {
|
||||||
<InputField
|
refetchChangeset?.();
|
||||||
name="name"
|
setTagCreationModalVisible(false);
|
||||||
label={t("tags.create.form.field.name.label")}
|
}}
|
||||||
onChange={val => setNewTagName(val)}
|
onError={setError}
|
||||||
value={newTagName}
|
tagCreationLink={(changeset._links["tag"] as Link).href}
|
||||||
/>
|
|
||||||
<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) => {
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Modal, InputField, Button, apiClient } from "@scm-manager/ui-components";
|
||||||
|
import { WithTranslation, withTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
type Props = WithTranslation & {
|
||||||
|
tagCreationLink: string;
|
||||||
|
tagNames: string[];
|
||||||
|
onClose: () => void;
|
||||||
|
onCreated: () => void;
|
||||||
|
onError: (error: Error) => void;
|
||||||
|
revision: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CreateTagModal: FC<Props> = ({ t, onClose, tagCreationLink, onCreated, onError, revision, tagNames }) => {
|
||||||
|
const [newTagName, setNewTagName] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const createTag = () => {
|
||||||
|
setLoading(true);
|
||||||
|
apiClient
|
||||||
|
.post(tagCreationLink, {
|
||||||
|
revision,
|
||||||
|
name: newTagName
|
||||||
|
})
|
||||||
|
.then(onCreated)
|
||||||
|
.catch(onError)
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
const isInvalid = () => {
|
||||||
|
return tagNames.includes(newTagName);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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}
|
||||||
|
validationError={isInvalid()}
|
||||||
|
errorMessage={t("tags.create.form.field.name.error")}
|
||||||
|
/>
|
||||||
|
<div className="mt-5">{t("tags.create.hint")}</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button action={onClose}>{t("tags.create.cancel")}</Button>
|
||||||
|
<Button color="success" action={() => createTag()} loading={loading} disabled={isInvalid()}>
|
||||||
|
{t("tags.create.confirm")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
closeFunction={onClose}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withTranslation("repos")(CreateTagModal);
|
||||||
Reference in New Issue
Block a user