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-syntaxhighlighting": "2.46.1-SNAPSHOT",
|
||||||
"@scm-manager/ui-tests": "2.46.1-SNAPSHOT",
|
"@scm-manager/ui-tests": "2.46.1-SNAPSHOT",
|
||||||
"@scm-manager/ui-text": "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-actions": "^6.4.20",
|
||||||
"@storybook/addon-essentials": "^6.4.20",
|
"@storybook/addon-essentials": "^6.4.20",
|
||||||
"@storybook/addon-interactions": "^6.4.20",
|
"@storybook/addon-interactions": "^6.4.20",
|
||||||
@@ -106,4 +108,4 @@
|
|||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -22,35 +22,19 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import React, { FC, ReactNode } from "react";
|
import React, { FC, ReactNode } from "react";
|
||||||
import classNames from "classnames";
|
import { CardList, Collapsible } from "@scm-manager/ui-layout";
|
||||||
import styled from "styled-components";
|
|
||||||
|
|
||||||
const Separator = styled.div`
|
|
||||||
border-bottom: 1px solid rgb(219, 219, 219, 0.5);
|
|
||||||
`;
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
namespaceHeader: ReactNode;
|
namespaceHeader: ReactNode;
|
||||||
elements: ReactNode[];
|
elements: ReactNode[];
|
||||||
|
collapsed?: boolean;
|
||||||
|
onCollapsedChange?: (collapsed: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const GroupEntries: FC<Props> = ({ namespaceHeader, elements }) => {
|
const GroupEntries: FC<Props> = ({ namespaceHeader, elements, collapsed, onCollapsedChange }) => (
|
||||||
const content = elements.map((entry, index) => (
|
<Collapsible className="mb-5" header={namespaceHeader} collapsed={collapsed} onCollapsedChange={onCollapsedChange}>
|
||||||
<React.Fragment key={index}>
|
<CardList>{elements}</CardList>
|
||||||
<div>{entry}</div>
|
</Collapsible>
|
||||||
{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" />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GroupEntries;
|
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 PrimaryContentColumn } from "./PrimaryContentColumn";
|
||||||
export { default as SecondaryNavigationColumn } from "./SecondaryNavigationColumn";
|
export { default as SecondaryNavigationColumn } from "./SecondaryNavigationColumn";
|
||||||
export { default as GroupEntries } from "./GroupEntries";
|
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
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import React, { FC, useState } from "react";
|
import React, { FC } from "react";
|
||||||
import { Repository } from "@scm-manager/ui-types";
|
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 RepositoryAvatar from "./RepositoryAvatar";
|
||||||
import { binder, ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions";
|
import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions";
|
||||||
import GroupEntry from "../layout/GroupEntry";
|
|
||||||
import RepositoryFlags from "./RepositoryFlags";
|
import RepositoryFlags from "./RepositoryFlags";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import Icon from "../Icon";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import classNames from "classnames";
|
import { useKeyboardIteratorTarget } from "@scm-manager/ui-shortcuts";
|
||||||
import { EXTENSION_POINT } from "../avatar/Avatar";
|
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;
|
type DateProp = Date | string;
|
||||||
|
|
||||||
@@ -43,125 +44,97 @@ type Props = {
|
|||||||
baseDate?: DateProp;
|
baseDate?: DateProp;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ContentRightContainer = styled.div`
|
const Avatar = styled.div`
|
||||||
height: calc(80px - 1.5rem);
|
.predefined-avatar {
|
||||||
|
height: 48px;
|
||||||
|
width: 48px;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const QuickAction = styled(Icon)`
|
const StyledLink = styled(Link)`
|
||||||
margin-top: 0.2rem;
|
overflow-wrap: anywhere;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ContactAvatar = styled.img`
|
const DescriptionRow = styled(Card.Row)`
|
||||||
max-width: 20px;
|
text-wrap: nowrap;
|
||||||
`;
|
overflow: hidden;
|
||||||
|
|
||||||
const ContactActionWrapper = styled.a`
|
|
||||||
height: 20px;
|
|
||||||
width: 20px;
|
|
||||||
padding-right: 2rem;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Name = styled.strong`
|
|
||||||
text-overflow: ellipsis;
|
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 RepositoryEntry: FC<Props> = ({ repository, baseDate }) => {
|
||||||
const [t] = useTranslation("repos");
|
const [t] = useTranslation("repos");
|
||||||
const [openCloneModal, setOpenCloneModal] = useState(false);
|
const ref = useKeyboardIteratorTarget();
|
||||||
|
|
||||||
const avatarFactory = binder.getExtension(EXTENSION_POINT);
|
const actions = () => (
|
||||||
|
<Menu>
|
||||||
const renderContactIcon = () => {
|
<Menu.DialogButton
|
||||||
if (avatarFactory) {
|
title={t("overview.clone")}
|
||||||
return (
|
description={
|
||||||
<ContactAvatar
|
<ExtensionPoint<extensionPoints.RepositoryDetailsInformation>
|
||||||
className="has-rounded-border"
|
name="repos.repository-details.information"
|
||||||
src={avatarFactory({ mail: repository.contact })}
|
renderAll={true}
|
||||||
alt={repository.contact}
|
props={{
|
||||||
/>
|
repository,
|
||||||
);
|
}}
|
||||||
}
|
/>
|
||||||
return <QuickAction className={classNames("is-clickable", "has-hover-visible")} name="envelope" color="info" />;
|
}
|
||||||
};
|
>
|
||||||
|
<Icon>download</Icon>
|
||||||
const createContentRight = () => (
|
{t("overview.clone")}
|
||||||
<ContentRightContainer
|
</Menu.DialogButton>
|
||||||
className={classNames(
|
{repository.contact ? (
|
||||||
"is-flex",
|
<Menu.ExternalLink
|
||||||
"is-flex-direction-column",
|
href={`mailto:${repository.contact}`}
|
||||||
"is-justify-content-space-between",
|
target="_blank"
|
||||||
"is-relative",
|
rel="noreferrer"
|
||||||
"mr-4"
|
title={t("overview.contact", { contact: repository.contact })}
|
||||||
)}
|
>
|
||||||
>
|
<Icon>envelope</Icon>
|
||||||
{openCloneModal && (
|
{t("overview.sendMailToContact")}
|
||||||
<Modal
|
</Menu.ExternalLink>
|
||||||
size="L"
|
) : null}
|
||||||
active={openCloneModal}
|
</Menu>
|
||||||
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 repositoryLink = `/repo/${repository.namespace}/${repository.name}/`;
|
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 (
|
return (
|
||||||
<>
|
<Card
|
||||||
<GroupEntry
|
as="li"
|
||||||
avatar={<RepositoryAvatar repository={repository} size={48} />}
|
aria-label={t("overview.ariaLabel", { name: repository.name })}
|
||||||
name={name}
|
action={<>{actions()}</>}
|
||||||
description={repository.description}
|
rowGap="0.25rem"
|
||||||
contentRight={actions}
|
avatar={
|
||||||
link={repositoryLink}
|
<Avatar className="is-align-self-flex-start">
|
||||||
ariaLabel={repository.name}
|
<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 React, { FC } from "react";
|
||||||
import { Color, Size } from "../styleConstants";
|
import { Color, Size } from "../styleConstants";
|
||||||
import Tooltip, { TooltipLocation } from "../Tooltip";
|
import { Card } from "@scm-manager/ui-layout";
|
||||||
import Tag from "../Tag";
|
import { Tooltip } from "@scm-manager/ui-overlays";
|
||||||
|
import { TooltipLocation } from "../Tooltip";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
color?: Color;
|
color?: Color;
|
||||||
@@ -36,10 +37,10 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const RepositoryFlag: FC<Props> = ({ children, title, size = "small", tooltipLocation = "bottom", ...props }) => (
|
const RepositoryFlag: FC<Props> = ({ children, title, size = "small", tooltipLocation = "bottom", ...props }) => (
|
||||||
<Tooltip location={tooltipLocation} message={title}>
|
<Tooltip side={tooltipLocation} message={title}>
|
||||||
<Tag size={size} {...props}>
|
<Card.Details.Detail.Tag {...props} className={`is-${size} is-relative`}>
|
||||||
{children}
|
{children}
|
||||||
</Tag>
|
</Card.Details.Detail.Tag>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -24,12 +24,12 @@
|
|||||||
import React, { FC, useState } from "react";
|
import React, { FC, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import styled from "styled-components";
|
|
||||||
import { Repository } from "@scm-manager/ui-types";
|
import { Repository } from "@scm-manager/ui-types";
|
||||||
import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions";
|
import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions";
|
||||||
import { TooltipLocation } from "../Tooltip";
|
import { TooltipLocation } from "../Tooltip";
|
||||||
import RepositoryFlag from "./RepositoryFlag";
|
import RepositoryFlag from "./RepositoryFlag";
|
||||||
import HealthCheckFailureDetail from "./HealthCheckFailureDetail";
|
import HealthCheckFailureDetail from "./HealthCheckFailureDetail";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
@@ -37,10 +37,8 @@ type Props = {
|
|||||||
tooltipLocation?: TooltipLocation;
|
tooltipLocation?: TooltipLocation;
|
||||||
};
|
};
|
||||||
|
|
||||||
const RepositoryFlagContainer = styled.div`
|
const GapedContainer = styled.div`
|
||||||
.tag {
|
gap: 0.5rem;
|
||||||
margin-left: 0.25rem;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const RepositoryFlags: FC<Props> = ({ repository, className, tooltipLocation = "right" }) => {
|
const RepositoryFlags: FC<Props> = ({ repository, className, tooltipLocation = "right" }) => {
|
||||||
@@ -86,17 +84,15 @@ const RepositoryFlags: FC<Props> = ({ repository, className, tooltipLocation = "
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
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}
|
{modal}
|
||||||
<RepositoryFlagContainer>
|
{repositoryFlags}
|
||||||
{repositoryFlags}
|
<ExtensionPoint<extensionPoints.RepositoryFlags>
|
||||||
<ExtensionPoint<extensionPoints.RepositoryFlags>
|
name="repository.flags"
|
||||||
name="repository.flags"
|
props={{ repository, tooltipLocation }}
|
||||||
props={{ repository, tooltipLocation }}
|
renderAll={true}
|
||||||
renderAll={true}
|
/>
|
||||||
/>
|
</GapedContainer>
|
||||||
</RepositoryFlagContainer>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import CSS from "csstype";
|
|||||||
|
|
||||||
type Props = HTMLAttributes<HTMLElement> & {
|
type Props = HTMLAttributes<HTMLElement> & {
|
||||||
action?: React.ReactElement;
|
action?: React.ReactElement;
|
||||||
|
avatar?: React.ReactElement;
|
||||||
/**
|
/**
|
||||||
* @default "div"
|
* @default "div"
|
||||||
*/
|
*/
|
||||||
@@ -48,7 +49,7 @@ type Props = HTMLAttributes<HTMLElement> & {
|
|||||||
* @since 2.44.0
|
* @since 2.44.0
|
||||||
*/
|
*/
|
||||||
const Card = React.forwardRef<HTMLElement, Props>(
|
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(
|
React.createElement(
|
||||||
Comp,
|
Comp,
|
||||||
{
|
{
|
||||||
@@ -56,13 +57,14 @@ const Card = React.forwardRef<HTMLElement, Props>(
|
|||||||
ref,
|
ref,
|
||||||
...props,
|
...props,
|
||||||
},
|
},
|
||||||
|
avatar ? avatar : null,
|
||||||
<div
|
<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 }}
|
style={{ gap: rowGap }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>,
|
</div>,
|
||||||
action ? <span className="ml-2">{action}</span> : null
|
action ? action : null
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -22,11 +22,12 @@
|
|||||||
* SOFTWARE.
|
* 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 * as RadixCollapsible from "@radix-ui/react-collapsible";
|
||||||
import { Icon } from "@scm-manager/ui-buttons";
|
import { Icon } from "@scm-manager/ui-buttons";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { useGeneratedId } from "@scm-manager/ui-components";
|
import { useGeneratedId } from "@scm-manager/ui-components";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
const StyledTrigger = styled(RadixCollapsible.Trigger)`
|
const StyledTrigger = styled(RadixCollapsible.Trigger)`
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
@@ -38,28 +39,49 @@ const StyledCollapsibleHeader = styled.div`
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
header: ReactNode;
|
header: ReactNode;
|
||||||
} & Pick<ComponentProps<typeof RadixCollapsible.Root>, "defaultOpen">;
|
defaultCollapsed?: boolean;
|
||||||
|
collapsed?: boolean;
|
||||||
|
onCollapsedChange?: (collapsed: boolean) => void;
|
||||||
|
} & Pick<ComponentProps<typeof RadixCollapsible.Root>, "className" | "children">;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @beta
|
* @beta
|
||||||
* @since 2.46.0
|
* @since 2.46.0
|
||||||
*/
|
*/
|
||||||
const Collapsible = React.forwardRef<HTMLButtonElement, Props>(({ children, header, defaultOpen }, ref) => {
|
const Collapsible = React.forwardRef<HTMLButtonElement, Props>(
|
||||||
const [open, setOpen] = useState(defaultOpen);
|
({ children, header, className, defaultCollapsed, collapsed, onCollapsedChange }, ref) => {
|
||||||
const titleId = useGeneratedId();
|
const [isCollapsed, setCollapsed] = useState(defaultCollapsed);
|
||||||
return (
|
const titleId = useGeneratedId();
|
||||||
<RadixCollapsible.Root className="card" open={open} onOpenChange={setOpen} defaultOpen={defaultOpen}>
|
useEffect(() => {
|
||||||
<StyledCollapsibleHeader className="card-header is-flex is-justify-content-space-between is-shadowless">
|
if (collapsed !== undefined) {
|
||||||
<span id={titleId} className="card-header-title">
|
setCollapsed(collapsed);
|
||||||
{header}
|
}
|
||||||
</span>
|
}, [collapsed]);
|
||||||
<StyledTrigger aria-labelledby={titleId} className="card-header-icon" ref={ref}>
|
|
||||||
<Icon>{open ? "angle-up" : "angle-down"}</Icon>
|
return (
|
||||||
</StyledTrigger>
|
<RadixCollapsible.Root
|
||||||
</StyledCollapsibleHeader>
|
className={classNames("card", className)}
|
||||||
<RadixCollapsible.Content className="card-content p-2">{children}</RadixCollapsible.Content>
|
open={!isCollapsed}
|
||||||
</RadixCollapsible.Root>
|
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;
|
export default Collapsible;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
|
|
||||||
.scmm-card {
|
.scmm-card {
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
*:is(h1, h2, h3, h4, h5, h6) a {
|
*:is(h1, h2, h3, h4, h5, h6) a {
|
||||||
color: var(--scm-secondary-text);
|
color: var(--scm-secondary-text);
|
||||||
|
|||||||
@@ -65,7 +65,9 @@
|
|||||||
"allNamespaces": "Alle Namespaces",
|
"allNamespaces": "Alle Namespaces",
|
||||||
"clone": "Clone/Checkout",
|
"clone": "Clone/Checkout",
|
||||||
"contact": "E-Mail senden an {{contact}}",
|
"contact": "E-Mail senden an {{contact}}",
|
||||||
"ariaLabel": "Repository {{name}}"
|
"ariaLabel": "Repository {{name}}",
|
||||||
|
"lastModified": "Letzte Änderung",
|
||||||
|
"sendMailToContact": "E-Mail an Kontakt senden"
|
||||||
},
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"title": "Repository hinzufügen",
|
"title": "Repository hinzufügen",
|
||||||
|
|||||||
@@ -65,7 +65,9 @@
|
|||||||
"allNamespaces": "All namespaces",
|
"allNamespaces": "All namespaces",
|
||||||
"clone": "Clone/Checkout",
|
"clone": "Clone/Checkout",
|
||||||
"contact": "Send mail to {{contact}}",
|
"contact": "Send mail to {{contact}}",
|
||||||
"ariaLabel": "Repository {{name}}"
|
"ariaLabel": "Repository {{name}}",
|
||||||
|
"lastModified": "Last modified",
|
||||||
|
"sendMailToContact": "Send mail to contact"
|
||||||
},
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"title": "Add Repository",
|
"title": "Add Repository",
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ const BranchTableWrapper: FC<Props> = ({ repository, baseUrl, data }) => {
|
|||||||
<BranchListWrapper className="is-flex is-flex-direction-column">
|
<BranchListWrapper className="is-flex is-flex-direction-column">
|
||||||
<KeyboardIterator>
|
<KeyboardIterator>
|
||||||
{activeBranches.length > 0 ? (
|
{activeBranches.length > 0 ? (
|
||||||
<Collapsible header={t("branches.table.branches.active")} defaultOpen>
|
<Collapsible header={t("branches.table.branches.active")}>
|
||||||
<BranchList
|
<BranchList
|
||||||
repository={repository}
|
repository={repository}
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
@@ -106,7 +106,7 @@ const BranchTableWrapper: FC<Props> = ({ repository, baseUrl, data }) => {
|
|||||||
</Collapsible>
|
</Collapsible>
|
||||||
) : null}
|
) : null}
|
||||||
{staleBranches.length > 0 ? (
|
{staleBranches.length > 0 ? (
|
||||||
<Collapsible header={t("branches.table.branches.stale")}>
|
<Collapsible header={t("branches.table.branches.stale")} defaultCollapsed>
|
||||||
<BranchList
|
<BranchList
|
||||||
repository={repository}
|
repository={repository}
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
|
|||||||
@@ -22,35 +22,18 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { NamespaceEntries, RepositoryEntry } from "@scm-manager/ui-components";
|
||||||
import { Icon, RepositoryEntry, GroupEntries } from "@scm-manager/ui-components";
|
|
||||||
import { RepositoryGroup } from "@scm-manager/ui-types";
|
import { RepositoryGroup } from "@scm-manager/ui-types";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
group: RepositoryGroup;
|
group: RepositoryGroup;
|
||||||
};
|
};
|
||||||
|
|
||||||
const RepositoryGroupEntry: FC<Props> = ({ group }) => {
|
const RepositoryGroupEntry: FC<Props> = ({ group }) => {
|
||||||
const [t] = useTranslation("namespaces");
|
const entries = group.repositories.map((repository) => {
|
||||||
|
return <RepositoryEntry repository={repository} key={repository.name} />;
|
||||||
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} />;
|
|
||||||
});
|
});
|
||||||
return <GroupEntries namespaceHeader={namespaceHeader} elements={entries} />;
|
return <NamespaceEntries group={group} elements={entries} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RepositoryGroupEntry;
|
export default RepositoryGroupEntry;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import React from "react";
|
import React, { FC } from "react";
|
||||||
|
|
||||||
import { NamespaceCollection, Repository } from "@scm-manager/ui-types";
|
import { NamespaceCollection, Repository } from "@scm-manager/ui-types";
|
||||||
|
|
||||||
@@ -33,19 +33,12 @@ type Props = {
|
|||||||
namespaces: NamespaceCollection;
|
namespaces: NamespaceCollection;
|
||||||
};
|
};
|
||||||
|
|
||||||
class RepositoryList extends React.Component<Props> {
|
const RepositoryList: FC<Props> = ({ repositories, namespaces }) => (
|
||||||
render() {
|
<>
|
||||||
const { repositories, namespaces } = this.props;
|
{groupByNamespace(repositories, namespaces).map((group) => (
|
||||||
|
<RepositoryGroupEntry group={group} key={group.name} />
|
||||||
const groups = groupByNamespace(repositories, namespaces);
|
))}
|
||||||
return (
|
</>
|
||||||
<div className="content">
|
);
|
||||||
{groups.map((group) => {
|
|
||||||
return <RepositoryGroupEntry group={group} key={group.name} />;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RepositoryList;
|
export default RepositoryList;
|
||||||
|
|||||||
Reference in New Issue
Block a user