improve ux

This commit is contained in:
Konstantin Schaper
2020-11-27 10:48:56 +01:00
parent 7895da0a2a
commit dc4607935b
4 changed files with 130 additions and 69 deletions

View File

@@ -95,7 +95,8 @@
"form": { "form": {
"field": { "field": {
"name": { "name": {
"label": "Name" "label": "Name",
"error": "Dieser Tag existiert bereits."
} }
} }
}, },

View File

@@ -95,7 +95,8 @@
"form": { "form": {
"field": { "field": {
"name": { "name": {
"label": "Name" "label": "Name",
"error": "This tag already exists."
} }
} }
}, },

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, 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) => {

View File

@@ -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);