mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-11 16:05:44 +01:00
Switch repo overview layout to cards
Squash commits of branch feature/repo_overview_cards: - Switch repo overview layout to cards - wip - Merge branch 'develop' into feature/repo_overview_cards - Fix collapsible - Fix collapsible - Fix styling - Merge branch 'develop' into feature/repo_overview_cards - Fix type for collapsible - Add changelog - Update storyshots - Merge branch 'develop' into feature/repo_overview_cards Committed-by: Thomas Zerr <thomas.zerr@cloudogu.com> Co-authored-by: tzerr <thomas.zerr@cloudogu.com>
This commit is contained in:
2
gradle/changelog/repo_overview_cards.yaml
Normal file
2
gradle/changelog/repo_overview_cards.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: changed
|
||||
description: Use card layout for repository overview
|
||||
@@ -28,6 +28,8 @@
|
||||
"@scm-manager/ui-syntaxhighlighting": "2.46.1-SNAPSHOT",
|
||||
"@scm-manager/ui-tests": "2.46.1-SNAPSHOT",
|
||||
"@scm-manager/ui-text": "2.46.1-SNAPSHOT",
|
||||
"@scm-manager/ui-layout": "2.46.1-SNAPSHOT",
|
||||
"@scm-manager/ui-overlays": "2.46.1-SNAPSHOT",
|
||||
"@storybook/addon-actions": "^6.4.20",
|
||||
"@storybook/addon-essentials": "^6.4.20",
|
||||
"@storybook/addon-interactions": "^6.4.20",
|
||||
@@ -106,4 +108,4 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,35 +22,19 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC, ReactNode } from "react";
|
||||
import classNames from "classnames";
|
||||
import styled from "styled-components";
|
||||
|
||||
const Separator = styled.div`
|
||||
border-bottom: 1px solid rgb(219, 219, 219, 0.5);
|
||||
`;
|
||||
import { CardList, Collapsible } from "@scm-manager/ui-layout";
|
||||
|
||||
type Props = {
|
||||
namespaceHeader: ReactNode;
|
||||
elements: ReactNode[];
|
||||
collapsed?: boolean;
|
||||
onCollapsedChange?: (collapsed: boolean) => void;
|
||||
};
|
||||
|
||||
const GroupEntries: FC<Props> = ({ namespaceHeader, elements }) => {
|
||||
const content = elements.map((entry, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<div>{entry}</div>
|
||||
{index + 1 !== elements.length ? <Separator className="mx-4" /> : null}
|
||||
</React.Fragment>
|
||||
));
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classNames("is-flex", "is-align-items-center", "is-size-6", "has-text-weight-bold", "p-3")}>
|
||||
{namespaceHeader}
|
||||
</div>
|
||||
<div className={classNames("box", "p-2")}>{content}</div>
|
||||
<div className="is-clearfix" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
const GroupEntries: FC<Props> = ({ namespaceHeader, elements, collapsed, onCollapsedChange }) => (
|
||||
<Collapsible className="mb-5" header={namespaceHeader} collapsed={collapsed} onCollapsedChange={onCollapsedChange}>
|
||||
<CardList>{elements}</CardList>
|
||||
</Collapsible>
|
||||
);
|
||||
|
||||
export default GroupEntries;
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
/*
|
||||
* 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 Icon from "../Icon";
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import React from "react";
|
||||
import GroupEntry from "./GroupEntry";
|
||||
import { Button, ButtonGroup } from "../buttons";
|
||||
import copyToClipboard from "../CopyToClipboard";
|
||||
|
||||
const link = "/foo/bar";
|
||||
const avatar = <Icon name="icons fa-2x fa-fw" alt="avatar" />;
|
||||
const name = <strong className="m-0">main content</strong>;
|
||||
const description = <small>more text</small>;
|
||||
const longName = (
|
||||
<strong className="m-0">
|
||||
Very-important-repository-with-a-particular-long-but-easily-rememberable-name-which-also-is-written-in-kebab-case
|
||||
</strong>
|
||||
);
|
||||
const contentRight = (
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
icon={"download"}
|
||||
title={"Copy clone command to clipboard"}
|
||||
action={() => copyToClipboard("git clone {url}")}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
);
|
||||
|
||||
storiesOf("GroupEntry", module)
|
||||
.addDecorator((story) => <MemoryRouter initialEntries={["/"]}>{story()}</MemoryRouter>)
|
||||
.addDecorator((storyFn) => <div className="m-5">{storyFn()}</div>)
|
||||
.add("Default", () => (
|
||||
<GroupEntry link={link} avatar={avatar} name={name} description={description} contentRight={contentRight} />
|
||||
))
|
||||
.add("With long texts", () => (
|
||||
<GroupEntry
|
||||
link={link}
|
||||
avatar={avatar}
|
||||
name={longName}
|
||||
description={
|
||||
<small>
|
||||
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et
|
||||
dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet
|
||||
clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet,
|
||||
consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,
|
||||
sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea
|
||||
takimata sanctus est Lorem ipsum dolor sit amet.
|
||||
</small>
|
||||
}
|
||||
contentRight={contentRight}
|
||||
/>
|
||||
));
|
||||
@@ -1,122 +0,0 @@
|
||||
/*
|
||||
* 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, ReactNode } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import classNames from "classnames";
|
||||
import styled from "styled-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useKeyboardIteratorTarget } from "@scm-manager/ui-shortcuts";
|
||||
|
||||
const StyledGroupEntry = styled.div`
|
||||
max-height: calc(90px - 1.5rem);
|
||||
width: 100%;
|
||||
pointer-events: all;
|
||||
`;
|
||||
|
||||
const OverlayLink = styled(Link)`
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
height: calc(90px - 1.5rem);
|
||||
pointer-events: all;
|
||||
border-radius: 4px;
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
const Avatar = styled.div`
|
||||
.predefined-avatar {
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const Description = styled.p`
|
||||
height: 1.5rem;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
overflow-y: visible;
|
||||
white-space: nowrap;
|
||||
word-break: break-all;
|
||||
`;
|
||||
|
||||
const ContentLeft = styled.div`
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
const ContentRight = styled.div`
|
||||
pointer-events: all;
|
||||
margin-bottom: -10px;
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
title?: string;
|
||||
avatar: string | ReactNode;
|
||||
name: string | ReactNode;
|
||||
description?: string | ReactNode;
|
||||
contentRight?: ReactNode;
|
||||
link: string;
|
||||
ariaLabel?: string;
|
||||
};
|
||||
|
||||
const GroupEntry: FC<Props> = ({ link, avatar, title, name, description, contentRight, ariaLabel }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
const ref = useKeyboardIteratorTarget();
|
||||
return (
|
||||
<div className="is-relative">
|
||||
<OverlayLink
|
||||
ref={ref}
|
||||
to={link}
|
||||
className="has-hover-background-blue"
|
||||
aria-label={t("overview.ariaLabel", { name: ariaLabel })}
|
||||
/>
|
||||
<StyledGroupEntry
|
||||
className={classNames("is-flex", "is-justify-content-space-between", "is-align-items-center", "p-2")}
|
||||
title={title}
|
||||
>
|
||||
<ContentLeft className={classNames("is-flex", "is-flex-grow-1", "is-align-items-center")}>
|
||||
<Avatar className="mr-4">{avatar}</Avatar>
|
||||
<div className={classNames("is-flex-grow-1", "is-clipped")}>
|
||||
<div className="mx-1">{name}</div>
|
||||
<Description className="mx-1">{description}</Description>
|
||||
</div>
|
||||
</ContentLeft>
|
||||
<ContentRight
|
||||
className={classNames(
|
||||
"is-hidden-touch",
|
||||
"is-flex",
|
||||
"is-flex-shrink-0",
|
||||
"is-justify-content-flex-end",
|
||||
"pl-5"
|
||||
)}
|
||||
>
|
||||
{contentRight}
|
||||
</ContentRight>
|
||||
</StyledGroupEntry>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupEntry;
|
||||
66
scm-ui/ui-components/src/layout/NamespaceEntries.tsx
Normal file
66
scm-ui/ui-components/src/layout/NamespaceEntries.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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, ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocalStorage } from "@scm-manager/ui-api";
|
||||
import { CardList, Collapsible } from "@scm-manager/ui-layout";
|
||||
import { RepositoryGroup } from "@scm-manager/ui-types";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Icon } from "../index";
|
||||
|
||||
type Props = {
|
||||
elements: ReactNode[];
|
||||
group: RepositoryGroup;
|
||||
};
|
||||
|
||||
const DefaultGroupHeader: FC<{ group: RepositoryGroup }> = ({ group }) => {
|
||||
const [t] = useTranslation("namespaces");
|
||||
return (
|
||||
<>
|
||||
<Link to={`/repos/${group.name}/`} className="has-text-inherit">
|
||||
<h3 className="has-text-weight-bold">{group.name}</h3>
|
||||
</Link>{" "}
|
||||
<Link to={`/namespace/${group.name}/settings`} aria-label={t("repositoryOverview.settings.tooltip")}>
|
||||
<Icon color="inherit" name="cog" title={t("repositoryOverview.settings.tooltip")} className="is-size-6 ml-2" />
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const NamespaceEntries: FC<Props> = ({ elements, group }) => {
|
||||
const [collapsed, setCollapsed] = useLocalStorage<boolean | null>(`repoNamespace.${group.name}.collapsed`, null);
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
collapsed={collapsed ?? false}
|
||||
onCollapsedChange={setCollapsed}
|
||||
className="mb-5"
|
||||
header={<DefaultGroupHeader group={group} />}
|
||||
>
|
||||
<CardList>{elements}</CardList>
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
|
||||
export default NamespaceEntries;
|
||||
@@ -36,3 +36,4 @@ export { default as CustomQueryFlexWrappedColumns } from "./CustomQueryFlexWrapp
|
||||
export { default as PrimaryContentColumn } from "./PrimaryContentColumn";
|
||||
export { default as SecondaryNavigationColumn } from "./SecondaryNavigationColumn";
|
||||
export { default as GroupEntries } from "./GroupEntries";
|
||||
export { default as NamespaceEntries } from "./NamespaceEntries";
|
||||
|
||||
@@ -21,18 +21,19 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC, useState } from "react";
|
||||
import React, { FC } from "react";
|
||||
import { Repository } from "@scm-manager/ui-types";
|
||||
import { DateFromNow, Modal } from "@scm-manager/ui-components";
|
||||
import { DateFromNow } from "@scm-manager/ui-components";
|
||||
import RepositoryAvatar from "./RepositoryAvatar";
|
||||
import { binder, ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions";
|
||||
import GroupEntry from "../layout/GroupEntry";
|
||||
import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions";
|
||||
import RepositoryFlags from "./RepositoryFlags";
|
||||
import styled from "styled-components";
|
||||
import Icon from "../Icon";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import classNames from "classnames";
|
||||
import { EXTENSION_POINT } from "../avatar/Avatar";
|
||||
import { useKeyboardIteratorTarget } from "@scm-manager/ui-shortcuts";
|
||||
import { Card } from "@scm-manager/ui-layout";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Menu } from "@scm-manager/ui-overlays";
|
||||
import { Icon } from "@scm-manager/ui-buttons";
|
||||
|
||||
type DateProp = Date | string;
|
||||
|
||||
@@ -43,125 +44,97 @@ type Props = {
|
||||
baseDate?: DateProp;
|
||||
};
|
||||
|
||||
const ContentRightContainer = styled.div`
|
||||
height: calc(80px - 1.5rem);
|
||||
const Avatar = styled.div`
|
||||
.predefined-avatar {
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const QuickAction = styled(Icon)`
|
||||
margin-top: 0.2rem;
|
||||
const StyledLink = styled(Link)`
|
||||
overflow-wrap: anywhere;
|
||||
`;
|
||||
|
||||
const ContactAvatar = styled.img`
|
||||
max-width: 20px;
|
||||
`;
|
||||
|
||||
const ContactActionWrapper = styled.a`
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
padding-right: 2rem;
|
||||
`;
|
||||
|
||||
const Name = styled.strong`
|
||||
const DescriptionRow = styled(Card.Row)`
|
||||
text-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
overflow-y: visible;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const DetailsRow = styled(Card.Row)`
|
||||
gap: 0.5rem;
|
||||
`;
|
||||
|
||||
const RepositoryEntry: FC<Props> = ({ repository, baseDate }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
const [openCloneModal, setOpenCloneModal] = useState(false);
|
||||
const ref = useKeyboardIteratorTarget();
|
||||
|
||||
const avatarFactory = binder.getExtension(EXTENSION_POINT);
|
||||
|
||||
const renderContactIcon = () => {
|
||||
if (avatarFactory) {
|
||||
return (
|
||||
<ContactAvatar
|
||||
className="has-rounded-border"
|
||||
src={avatarFactory({ mail: repository.contact })}
|
||||
alt={repository.contact}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <QuickAction className={classNames("is-clickable", "has-hover-visible")} name="envelope" color="info" />;
|
||||
};
|
||||
|
||||
const createContentRight = () => (
|
||||
<ContentRightContainer
|
||||
className={classNames(
|
||||
"is-flex",
|
||||
"is-flex-direction-column",
|
||||
"is-justify-content-space-between",
|
||||
"is-relative",
|
||||
"mr-4"
|
||||
)}
|
||||
>
|
||||
{openCloneModal && (
|
||||
<Modal
|
||||
size="L"
|
||||
active={openCloneModal}
|
||||
title={t("overview.clone")}
|
||||
body={
|
||||
<ExtensionPoint<extensionPoints.RepositoryDetailsInformation>
|
||||
name="repos.repository-details.information"
|
||||
renderAll={true}
|
||||
props={{
|
||||
repository
|
||||
}}
|
||||
/>
|
||||
}
|
||||
closeFunction={() => setOpenCloneModal(false)}
|
||||
/>
|
||||
)}
|
||||
<span className={classNames("is-flex", "is-justify-content-flex-end", "is-align-items-center")}>
|
||||
{repository.contact ? (
|
||||
<ContactActionWrapper
|
||||
href={`mailto:${repository.contact}`}
|
||||
target="_blank"
|
||||
className={"is-size-5"}
|
||||
title={t("overview.contact", { contact: repository.contact })}
|
||||
>
|
||||
{renderContactIcon()}
|
||||
</ContactActionWrapper>
|
||||
) : null}
|
||||
<QuickAction
|
||||
className={classNames("is-clickable", "is-size-5", "has-hover-visible")}
|
||||
name="download"
|
||||
color="info"
|
||||
onClick={() => setOpenCloneModal(true)}
|
||||
title={t("overview.clone")}
|
||||
/>
|
||||
</span>
|
||||
<small className="pb-1">
|
||||
<DateFromNow baseDate={baseDate} date={repository.lastModified || repository.creationDate} />
|
||||
</small>
|
||||
</ContentRightContainer>
|
||||
const actions = () => (
|
||||
<Menu>
|
||||
<Menu.DialogButton
|
||||
title={t("overview.clone")}
|
||||
description={
|
||||
<ExtensionPoint<extensionPoints.RepositoryDetailsInformation>
|
||||
name="repos.repository-details.information"
|
||||
renderAll={true}
|
||||
props={{
|
||||
repository,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Icon>download</Icon>
|
||||
{t("overview.clone")}
|
||||
</Menu.DialogButton>
|
||||
{repository.contact ? (
|
||||
<Menu.ExternalLink
|
||||
href={`mailto:${repository.contact}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title={t("overview.contact", { contact: repository.contact })}
|
||||
>
|
||||
<Icon>envelope</Icon>
|
||||
{t("overview.sendMailToContact")}
|
||||
</Menu.ExternalLink>
|
||||
) : null}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const repositoryLink = `/repo/${repository.namespace}/${repository.name}/`;
|
||||
const actions = createContentRight();
|
||||
const name = (
|
||||
<div className="is-flex">
|
||||
<ExtensionPoint<extensionPoints.RepositoryCardBeforeTitle>
|
||||
name="repository.card.beforeTitle"
|
||||
props={{ repository }}
|
||||
/>
|
||||
<Name>{repository.name}</Name> <RepositoryFlags repository={repository} className="is-hidden-mobile" />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GroupEntry
|
||||
avatar={<RepositoryAvatar repository={repository} size={48} />}
|
||||
name={name}
|
||||
description={repository.description}
|
||||
contentRight={actions}
|
||||
link={repositoryLink}
|
||||
ariaLabel={repository.name}
|
||||
/>
|
||||
</>
|
||||
<Card
|
||||
as="li"
|
||||
aria-label={t("overview.ariaLabel", { name: repository.name })}
|
||||
action={<>{actions()}</>}
|
||||
rowGap="0.25rem"
|
||||
avatar={
|
||||
<Avatar className="is-align-self-flex-start">
|
||||
<RepositoryAvatar repository={repository} size={48} />
|
||||
</Avatar>
|
||||
}
|
||||
>
|
||||
<Card.Row className="is-flex">
|
||||
<ExtensionPoint<extensionPoints.RepositoryCardBeforeTitle>
|
||||
name="repository.card.beforeTitle"
|
||||
props={{ repository }}
|
||||
/>
|
||||
<Card.Title level={4}>
|
||||
<StyledLink to={repositoryLink} ref={ref}>
|
||||
{repository.name}{" "}
|
||||
</StyledLink>
|
||||
</Card.Title>
|
||||
</Card.Row>
|
||||
<DescriptionRow className="is-size-7">{repository.description}</DescriptionRow>
|
||||
<DetailsRow className="is-flex is-align-items-center is-justify-content-space-between is-flex-wrap-wrap">
|
||||
<span className="is-size-7 has-text-secondary is-relative">
|
||||
{t("overview.lastModified")}{" "}
|
||||
<DateFromNow baseDate={baseDate} date={repository.lastModified ?? repository.creationDate} />
|
||||
</span>
|
||||
<RepositoryFlags repository={repository} />
|
||||
</DetailsRow>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -24,8 +24,9 @@
|
||||
|
||||
import React, { FC } from "react";
|
||||
import { Color, Size } from "../styleConstants";
|
||||
import Tooltip, { TooltipLocation } from "../Tooltip";
|
||||
import Tag from "../Tag";
|
||||
import { Card } from "@scm-manager/ui-layout";
|
||||
import { Tooltip } from "@scm-manager/ui-overlays";
|
||||
import { TooltipLocation } from "../Tooltip";
|
||||
|
||||
type Props = {
|
||||
color?: Color;
|
||||
@@ -36,10 +37,10 @@ type Props = {
|
||||
};
|
||||
|
||||
const RepositoryFlag: FC<Props> = ({ children, title, size = "small", tooltipLocation = "bottom", ...props }) => (
|
||||
<Tooltip location={tooltipLocation} message={title}>
|
||||
<Tag size={size} {...props}>
|
||||
<Tooltip side={tooltipLocation} message={title}>
|
||||
<Card.Details.Detail.Tag {...props} className={`is-${size} is-relative`}>
|
||||
{children}
|
||||
</Tag>
|
||||
</Card.Details.Detail.Tag>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
|
||||
@@ -24,12 +24,12 @@
|
||||
import React, { FC, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import classNames from "classnames";
|
||||
import styled from "styled-components";
|
||||
import { Repository } from "@scm-manager/ui-types";
|
||||
import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions";
|
||||
import { TooltipLocation } from "../Tooltip";
|
||||
import RepositoryFlag from "./RepositoryFlag";
|
||||
import HealthCheckFailureDetail from "./HealthCheckFailureDetail";
|
||||
import styled from "styled-components";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
@@ -37,10 +37,8 @@ type Props = {
|
||||
tooltipLocation?: TooltipLocation;
|
||||
};
|
||||
|
||||
const RepositoryFlagContainer = styled.div`
|
||||
.tag {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
const GapedContainer = styled.div`
|
||||
gap: 0.5rem;
|
||||
`;
|
||||
|
||||
const RepositoryFlags: FC<Props> = ({ repository, className, tooltipLocation = "right" }) => {
|
||||
@@ -86,17 +84,15 @@ const RepositoryFlags: FC<Props> = ({ repository, className, tooltipLocation = "
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classNames("is-flex", "is-align-items-center", className)}>
|
||||
<GapedContainer className={classNames("is-flex", "is-align-items-center", "is-flex-wrap-wrap", className)}>
|
||||
{modal}
|
||||
<RepositoryFlagContainer>
|
||||
{repositoryFlags}
|
||||
<ExtensionPoint<extensionPoints.RepositoryFlags>
|
||||
name="repository.flags"
|
||||
props={{ repository, tooltipLocation }}
|
||||
renderAll={true}
|
||||
/>
|
||||
</RepositoryFlagContainer>
|
||||
</div>
|
||||
{repositoryFlags}
|
||||
<ExtensionPoint<extensionPoints.RepositoryFlags>
|
||||
name="repository.flags"
|
||||
props={{ repository, tooltipLocation }}
|
||||
renderAll={true}
|
||||
/>
|
||||
</GapedContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import CSS from "csstype";
|
||||
|
||||
type Props = HTMLAttributes<HTMLElement> & {
|
||||
action?: React.ReactElement;
|
||||
avatar?: React.ReactElement;
|
||||
/**
|
||||
* @default "div"
|
||||
*/
|
||||
@@ -48,7 +49,7 @@ type Props = HTMLAttributes<HTMLElement> & {
|
||||
* @since 2.44.0
|
||||
*/
|
||||
const Card = React.forwardRef<HTMLElement, Props>(
|
||||
({ className, rowGap = "0.5rem", children, as: Comp = "div", action, ...props }, ref) =>
|
||||
({ className, avatar, rowGap = "0.5rem", children, as: Comp = "div", action, ...props }, ref) =>
|
||||
React.createElement(
|
||||
Comp,
|
||||
{
|
||||
@@ -56,13 +57,14 @@ const Card = React.forwardRef<HTMLElement, Props>(
|
||||
ref,
|
||||
...props,
|
||||
},
|
||||
avatar ? avatar : null,
|
||||
<div
|
||||
className="is-flex is-flex-direction-column is-justify-content-center is-flex-grow-1"
|
||||
className="is-flex is-flex-direction-column is-justify-content-center is-flex-grow-1 is-clipped"
|
||||
style={{ gap: rowGap }}
|
||||
>
|
||||
{children}
|
||||
</div>,
|
||||
action ? <span className="ml-2">{action}</span> : null
|
||||
action ? action : null
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -22,11 +22,12 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React, { ComponentProps, ReactNode, useState } from "react";
|
||||
import React, { ComponentProps, ReactNode, useEffect, useState } from "react";
|
||||
import * as RadixCollapsible from "@radix-ui/react-collapsible";
|
||||
import { Icon } from "@scm-manager/ui-buttons";
|
||||
import styled from "styled-components";
|
||||
import { useGeneratedId } from "@scm-manager/ui-components";
|
||||
import classNames from "classnames";
|
||||
|
||||
const StyledTrigger = styled(RadixCollapsible.Trigger)`
|
||||
margin-right: 0.5rem;
|
||||
@@ -38,28 +39,49 @@ const StyledCollapsibleHeader = styled.div`
|
||||
|
||||
type Props = {
|
||||
header: ReactNode;
|
||||
} & Pick<ComponentProps<typeof RadixCollapsible.Root>, "defaultOpen">;
|
||||
defaultCollapsed?: boolean;
|
||||
collapsed?: boolean;
|
||||
onCollapsedChange?: (collapsed: boolean) => void;
|
||||
} & Pick<ComponentProps<typeof RadixCollapsible.Root>, "className" | "children">;
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.46.0
|
||||
*/
|
||||
const Collapsible = React.forwardRef<HTMLButtonElement, Props>(({ children, header, defaultOpen }, ref) => {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
const titleId = useGeneratedId();
|
||||
return (
|
||||
<RadixCollapsible.Root className="card" open={open} onOpenChange={setOpen} defaultOpen={defaultOpen}>
|
||||
<StyledCollapsibleHeader className="card-header is-flex is-justify-content-space-between is-shadowless">
|
||||
<span id={titleId} className="card-header-title">
|
||||
{header}
|
||||
</span>
|
||||
<StyledTrigger aria-labelledby={titleId} className="card-header-icon" ref={ref}>
|
||||
<Icon>{open ? "angle-up" : "angle-down"}</Icon>
|
||||
</StyledTrigger>
|
||||
</StyledCollapsibleHeader>
|
||||
<RadixCollapsible.Content className="card-content p-2">{children}</RadixCollapsible.Content>
|
||||
</RadixCollapsible.Root>
|
||||
);
|
||||
});
|
||||
const Collapsible = React.forwardRef<HTMLButtonElement, Props>(
|
||||
({ children, header, className, defaultCollapsed, collapsed, onCollapsedChange }, ref) => {
|
||||
const [isCollapsed, setCollapsed] = useState(defaultCollapsed);
|
||||
const titleId = useGeneratedId();
|
||||
useEffect(() => {
|
||||
if (collapsed !== undefined) {
|
||||
setCollapsed(collapsed);
|
||||
}
|
||||
}, [collapsed]);
|
||||
|
||||
return (
|
||||
<RadixCollapsible.Root
|
||||
className={classNames("card", className)}
|
||||
open={!isCollapsed}
|
||||
onOpenChange={(o) => {
|
||||
setCollapsed(!o);
|
||||
if (onCollapsedChange) {
|
||||
onCollapsedChange(!o);
|
||||
}
|
||||
}}
|
||||
defaultOpen={!defaultCollapsed}
|
||||
>
|
||||
<StyledCollapsibleHeader className="card-header is-flex is-justify-content-space-between is-shadowless">
|
||||
<span id={titleId} className="card-header-title">
|
||||
{header}
|
||||
</span>
|
||||
<StyledTrigger aria-labelledby={titleId} className="card-header-icon" ref={ref}>
|
||||
<Icon>{isCollapsed ? "angle-left" : "angle-down"}</Icon>
|
||||
</StyledTrigger>
|
||||
</StyledCollapsibleHeader>
|
||||
<RadixCollapsible.Content className="card-content p-2">{children}</RadixCollapsible.Content>
|
||||
</RadixCollapsible.Root>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default Collapsible;
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
.scmm-card {
|
||||
padding: 0.5rem;
|
||||
gap: 0.5rem;
|
||||
|
||||
*:is(h1, h2, h3, h4, h5, h6) a {
|
||||
color: var(--scm-secondary-text);
|
||||
|
||||
@@ -65,7 +65,9 @@
|
||||
"allNamespaces": "Alle Namespaces",
|
||||
"clone": "Clone/Checkout",
|
||||
"contact": "E-Mail senden an {{contact}}",
|
||||
"ariaLabel": "Repository {{name}}"
|
||||
"ariaLabel": "Repository {{name}}",
|
||||
"lastModified": "Letzte Änderung",
|
||||
"sendMailToContact": "E-Mail an Kontakt senden"
|
||||
},
|
||||
"create": {
|
||||
"title": "Repository hinzufügen",
|
||||
|
||||
@@ -65,7 +65,9 @@
|
||||
"allNamespaces": "All namespaces",
|
||||
"clone": "Clone/Checkout",
|
||||
"contact": "Send mail to {{contact}}",
|
||||
"ariaLabel": "Repository {{name}}"
|
||||
"ariaLabel": "Repository {{name}}",
|
||||
"lastModified": "Last modified",
|
||||
"sendMailToContact": "Send mail to contact"
|
||||
},
|
||||
"create": {
|
||||
"title": "Add Repository",
|
||||
|
||||
@@ -96,7 +96,7 @@ const BranchTableWrapper: FC<Props> = ({ repository, baseUrl, data }) => {
|
||||
<BranchListWrapper className="is-flex is-flex-direction-column">
|
||||
<KeyboardIterator>
|
||||
{activeBranches.length > 0 ? (
|
||||
<Collapsible header={t("branches.table.branches.active")} defaultOpen>
|
||||
<Collapsible header={t("branches.table.branches.active")}>
|
||||
<BranchList
|
||||
repository={repository}
|
||||
baseUrl={baseUrl}
|
||||
@@ -106,7 +106,7 @@ const BranchTableWrapper: FC<Props> = ({ repository, baseUrl, data }) => {
|
||||
</Collapsible>
|
||||
) : null}
|
||||
{staleBranches.length > 0 ? (
|
||||
<Collapsible header={t("branches.table.branches.stale")}>
|
||||
<Collapsible header={t("branches.table.branches.stale")} defaultCollapsed>
|
||||
<BranchList
|
||||
repository={repository}
|
||||
baseUrl={baseUrl}
|
||||
|
||||
@@ -22,35 +22,18 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Icon, RepositoryEntry, GroupEntries } from "@scm-manager/ui-components";
|
||||
import { NamespaceEntries, RepositoryEntry } from "@scm-manager/ui-components";
|
||||
import { RepositoryGroup } from "@scm-manager/ui-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
group: RepositoryGroup;
|
||||
};
|
||||
|
||||
const RepositoryGroupEntry: FC<Props> = ({ group }) => {
|
||||
const [t] = useTranslation("namespaces");
|
||||
|
||||
const settingsLink = group.namespace?._links?.permissions && (
|
||||
<Link to={`/namespace/${group.name}/settings`} aria-label={t("repositoryOverview.settings.tooltip")}>
|
||||
<Icon color="inherit" name="cog" title={t("repositoryOverview.settings.tooltip")} className="is-size-6 ml-2" />
|
||||
</Link>
|
||||
);
|
||||
const namespaceHeader = (
|
||||
<>
|
||||
<Link to={`/repos/${group.name}/`} className="has-text-inherit">
|
||||
{group.name}
|
||||
</Link>{" "}
|
||||
{settingsLink}
|
||||
</>
|
||||
);
|
||||
const entries = group.repositories.map((repository, index) => {
|
||||
return <RepositoryEntry repository={repository} key={index} />;
|
||||
const entries = group.repositories.map((repository) => {
|
||||
return <RepositoryEntry repository={repository} key={repository.name} />;
|
||||
});
|
||||
return <GroupEntries namespaceHeader={namespaceHeader} elements={entries} />;
|
||||
return <NamespaceEntries group={group} elements={entries} />;
|
||||
};
|
||||
|
||||
export default RepositoryGroupEntry;
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import React, { FC } from "react";
|
||||
|
||||
import { NamespaceCollection, Repository } from "@scm-manager/ui-types";
|
||||
|
||||
@@ -33,19 +33,12 @@ type Props = {
|
||||
namespaces: NamespaceCollection;
|
||||
};
|
||||
|
||||
class RepositoryList extends React.Component<Props> {
|
||||
render() {
|
||||
const { repositories, namespaces } = this.props;
|
||||
|
||||
const groups = groupByNamespace(repositories, namespaces);
|
||||
return (
|
||||
<div className="content">
|
||||
{groups.map((group) => {
|
||||
return <RepositoryGroupEntry group={group} key={group.name} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
const RepositoryList: FC<Props> = ({ repositories, namespaces }) => (
|
||||
<>
|
||||
{groupByNamespace(repositories, namespaces).map((group) => (
|
||||
<RepositoryGroupEntry group={group} key={group.name} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
export default RepositoryList;
|
||||
|
||||
Reference in New Issue
Block a user