mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-07-07 16:41:45 +02:00
Apply new design for AheadBehindTag
Committed-by: Konstantin Schaper <konstantin.schaper@cloudogu.com> Co-authored-by: tarik-guersoy <tarik.guersoy@cloudogu.com> Co-authored-by: Konstantin Schaper <konstantin.schaper@cloudogu.com>
This commit is contained in:
2
gradle/changelog/ahead-behind.yaml
Normal file
2
gradle/changelog/ahead-behind.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: changed
|
||||
description: Apply new design to ahead behind tag
|
||||
@@ -22,7 +22,7 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React, { ComponentType, ReactNode } from "react";
|
||||
import React, { ComponentType, FC, ReactNode } from "react";
|
||||
import {
|
||||
Branch,
|
||||
Changeset,
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
import { ExtensionPointDefinition } from "./binder";
|
||||
import { RenderableExtensionPointDefinition, SimpleRenderableDynamicExtensionPointDefinition } from "./ExtensionPoint";
|
||||
import ExtractProps from "./extractProps";
|
||||
import { useGeneratedId } from "@scm-manager/ui-components";
|
||||
|
||||
type RepositoryCreatorSubFormProps = {
|
||||
repository: RepositoryCreation;
|
||||
@@ -684,17 +685,22 @@ export type GroupInformationTableBottom = RenderableExtensionPointDefinition<
|
||||
type BranchListDetailProps = {
|
||||
repository: Repository;
|
||||
branch: Branch;
|
||||
labelId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @since 2.46.0
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Card } from "@scm-manager/ui-layout";
|
||||
* import { extensionPoints, binder } from "@scm-manager/ui-extensions";
|
||||
*
|
||||
* binder.bind<extensionPoints.BranchListDetail>(
|
||||
* "branches.list.detail",
|
||||
* <Card.Details.Detail>
|
||||
* <Card.Details.Detail.Label>Reviewers</Card.Details.Detail.Label>
|
||||
* <Card.Details.Detail.Tag>2 / 3</Card.Details.Detail.Tag>
|
||||
* </Card.Details.Detail>
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
export type BranchListDetail = ExtensionPointDefinition<
|
||||
"branches.list.detail",
|
||||
{
|
||||
name: string;
|
||||
render: (props: BranchListDetailProps) => ReactNode | null;
|
||||
},
|
||||
Omit<BranchListDetailProps, "labelId">
|
||||
>;
|
||||
export type BranchListDetail = RenderableExtensionPointDefinition<"branches.list.detail", BranchListDetailProps>;
|
||||
@@ -24,7 +24,11 @@
|
||||
|
||||
export { default as binder, Binder, ExtensionPointDefinition } from "./binder";
|
||||
export * from "./useBinder";
|
||||
export { default as ExtensionPoint } from "./ExtensionPoint";
|
||||
export {
|
||||
default as ExtensionPoint,
|
||||
RenderableExtensionPointDefinition,
|
||||
SimpleRenderableDynamicExtensionPointDefinition,
|
||||
} from "./ExtensionPoint";
|
||||
export { default as ExtractProps } from "./extractProps";
|
||||
|
||||
// suppress eslint prettier warning,
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
"@scm-manager/prettier-config": "^2.10.1",
|
||||
"@scm-manager/tsconfig": "^2.13.0",
|
||||
"@scm-manager/ui-styles": "2.45.3-SNAPSHOT",
|
||||
"@scm-manager/ui-overlays": "2.45.3-SNAPSHOT",
|
||||
"@scm-manager/ui-buttons": "2.45.3-SNAPSHOT",
|
||||
"@storybook/addon-actions": "^6.5.10",
|
||||
"@storybook/addon-docs": "^6.5.14",
|
||||
"@storybook/addon-essentials": "^6.5.10",
|
||||
@@ -39,7 +37,8 @@
|
||||
"react-dom": "17",
|
||||
"styled-components": "5",
|
||||
"@scm-manager/ui-buttons": "2.45.3-SNAPSHOT",
|
||||
"@scm-manager/ui-overlays": "2.45.3-SNAPSHOT"
|
||||
"@scm-manager/ui-overlays": "2.45.3-SNAPSHOT",
|
||||
"@scm-manager/ui-components": "2.45.3-SNAPSHOT"
|
||||
},
|
||||
"prettier": "@scm-manager/prettier-config",
|
||||
"eslintConfig": {
|
||||
|
||||
@@ -32,6 +32,7 @@ import CardTitle from "../card/CardTitle";
|
||||
import { Menu } from "@scm-manager/ui-overlays";
|
||||
import { Icon } from "@scm-manager/ui-buttons";
|
||||
import CardRow from "../card/CardRow";
|
||||
import { CardDetail, CardDetailLabel, CardDetails, CardDetailTag } from "../card/CardDetail";
|
||||
|
||||
export default {
|
||||
title: "CardList",
|
||||
@@ -140,5 +141,30 @@ Default.args = {
|
||||
<span>(OPEN)</span>
|
||||
</CardRow>
|
||||
</CardListCard>,
|
||||
<CardListCard>
|
||||
<CardTitle>
|
||||
<Link aria-label="Edit My least liked repo" to="/cards/1">
|
||||
My least liked repo
|
||||
</Link>
|
||||
</CardTitle>
|
||||
<CardRow>
|
||||
<CardDetails>
|
||||
<CardDetail>
|
||||
{({ labelId }) => (
|
||||
<>
|
||||
<CardDetailLabel id={labelId}>Workers</CardDetailLabel>
|
||||
<CardDetailTag aria-labelledby={labelId}>2/3</CardDetailTag>
|
||||
</>
|
||||
)}
|
||||
</CardDetail>
|
||||
<CardDetail>
|
||||
<CardDetailLabel>MyCustomDetail</CardDetailLabel>
|
||||
<a className="is-relative" href="https://scm-manager.org">
|
||||
Docs
|
||||
</a>
|
||||
</CardDetail>
|
||||
</CardDetails>
|
||||
</CardRow>
|
||||
</CardListCard>,
|
||||
],
|
||||
} as ComponentProps<typeof CardListBox>;
|
||||
|
||||
@@ -30,6 +30,7 @@ import Card from "../card/Card";
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.44.0
|
||||
* @see Card
|
||||
*/
|
||||
export const CardListCard = React.forwardRef<HTMLElement, Omit<ComponentProps<typeof Card>, "as">>((props, ref) => (
|
||||
<Card ref={ref} {...props} as="li" />
|
||||
|
||||
72
scm-ui/ui-layout/src/card/Card.stories.tsx
Normal file
72
scm-ui/ui-layout/src/card/Card.stories.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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 StoryRouter from "storybook-react-router";
|
||||
import { ComponentMeta, StoryFn } from "@storybook/react";
|
||||
import React, { ComponentProps } from "react";
|
||||
import { ExtractProps } from "@scm-manager/ui-extensions";
|
||||
import { Link } from "react-router-dom";
|
||||
import CardTitle from "../card/CardTitle";
|
||||
import CardRow from "../card/CardRow";
|
||||
import { CardDetail, CardDetailLabel, CardDetails, CardDetailTag } from "./CardDetail";
|
||||
import Card from "./Card";
|
||||
|
||||
export default {
|
||||
title: "Card",
|
||||
component: Card,
|
||||
decorators: [StoryRouter()],
|
||||
} as ComponentMeta<typeof Card>;
|
||||
|
||||
const Template: StoryFn<ExtractProps<typeof Card>> = (args) => <Card {...args} />;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
||||
Default.args = {
|
||||
className: "box",
|
||||
children: [
|
||||
<CardTitle>
|
||||
<Link aria-label="Edit My least liked repo" to="/cards/1">
|
||||
My least liked repo
|
||||
</Link>
|
||||
</CardTitle>,
|
||||
<CardRow>
|
||||
<CardDetails>
|
||||
<CardDetail>
|
||||
{({ labelId }) => (
|
||||
<>
|
||||
<CardDetailLabel id={labelId}>Workers</CardDetailLabel>
|
||||
<CardDetailTag aria-labelledby={labelId}>2/3</CardDetailTag>
|
||||
</>
|
||||
)}
|
||||
</CardDetail>
|
||||
<CardDetail>
|
||||
<CardDetailLabel>MyCustomDetail</CardDetailLabel>
|
||||
<a className="is-relative" href="https://scm-manager.org">
|
||||
Docs
|
||||
</a>
|
||||
</CardDetail>
|
||||
</CardDetails>
|
||||
</CardRow>,
|
||||
],
|
||||
} as ComponentProps<typeof Card>;
|
||||
@@ -46,6 +46,9 @@ type Props = HTMLAttributes<HTMLElement> & {
|
||||
};
|
||||
|
||||
/**
|
||||
* If the Card's title contains a link, the whole Card becomes a click target for that link.
|
||||
* Because of this, any interactive elements require the <code>is-relative</code> class to receive cursor events.
|
||||
*
|
||||
* @beta
|
||||
* @since 2.44.0
|
||||
*/
|
||||
|
||||
91
scm-ui/ui-layout/src/card/CardDetail.tsx
Normal file
91
scm-ui/ui-layout/src/card/CardDetail.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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, { HTMLAttributes, ReactNode } from "react";
|
||||
import classNames from "classnames";
|
||||
import { useGeneratedId } from "@scm-manager/ui-components";
|
||||
import styled from "styled-components";
|
||||
|
||||
type CardDetailProps = HTMLAttributes<HTMLSpanElement> & {
|
||||
children: ReactNode | ((props: { labelId: string }) => ReactNode);
|
||||
};
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.46.0
|
||||
*/
|
||||
export const CardDetail = React.forwardRef<HTMLSpanElement, CardDetailProps>(
|
||||
({ children, className, ...props }, ref) => {
|
||||
const labelId = useGeneratedId();
|
||||
return (
|
||||
<span {...props} className={classNames("is-flex is-align-items-center", className)} ref={ref}>
|
||||
{typeof children === "function" ? children({ labelId }) : children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.46.0
|
||||
*/
|
||||
export const CardDetailLabel = React.forwardRef<HTMLSpanElement, HTMLAttributes<HTMLSpanElement>>(
|
||||
({ children, className, ...props }, ref) => (
|
||||
<span {...props} className={classNames("has-text-secondary is-size-7 mr-1", className)} ref={ref}>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.46.0
|
||||
*/
|
||||
export const CardDetailTag = React.forwardRef<HTMLSpanElement, HTMLAttributes<HTMLSpanElement>>(
|
||||
({ children, className, ...props }, ref) => (
|
||||
<span {...props} className={classNames("tag is-rounded is-light", className)} ref={ref}>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
);
|
||||
|
||||
const CardDetailsRowContainer = styled.div`
|
||||
gap: 0.5rem 1rem;
|
||||
`;
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.46.0
|
||||
*/
|
||||
export const CardDetails = React.forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
||||
({ children, className, ...props }, ref) => (
|
||||
<CardDetailsRowContainer
|
||||
{...props}
|
||||
className={classNames("is-flex is-flex-wrap-wrap is-align-items-center", className)}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</CardDetailsRowContainer>
|
||||
)
|
||||
);
|
||||
@@ -25,15 +25,27 @@
|
||||
import CardListComponent, { CardListBox as CardListBoxComponent, CardListCard } from "./card-list/CardList";
|
||||
import CardTitle from "./card/CardTitle";
|
||||
import CardRow from "./card/CardRow";
|
||||
import { CardDetail, CardDetailLabel, CardDetails, CardDetailTag } from "./card/CardDetail";
|
||||
import CardComponent from "./card/Card";
|
||||
|
||||
export { default as Collapsible } from "./collapsible/Collapsible";
|
||||
|
||||
const CardListExport = {
|
||||
Card: Object.assign(CardListCard, {
|
||||
Row: CardRow,
|
||||
Title: CardTitle,
|
||||
const CardExport = {
|
||||
Title: CardTitle,
|
||||
Row: CardRow,
|
||||
Details: Object.assign(CardDetails, {
|
||||
Detail: Object.assign(CardDetail, {
|
||||
Label: CardDetailLabel,
|
||||
Tag: CardDetailTag,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
export const Card = Object.assign(CardComponent, CardExport);
|
||||
|
||||
const CardListExport = {
|
||||
Card: Object.assign(CardListCard, CardExport),
|
||||
};
|
||||
|
||||
export const CardList = Object.assign(CardListComponent, CardListExport);
|
||||
export const CardListBox = Object.assign(CardListBoxComponent, CardListExport);
|
||||
|
||||
@@ -158,8 +158,9 @@
|
||||
"dangerZone": "Branch löschen",
|
||||
"aheadBehind": {
|
||||
"tooltip": "{{ahead}} Commit(s) vor, {{behind}} Commit(s) hinter dem Default Branch",
|
||||
"aheadLabel": "{{count}} voraus",
|
||||
"behindLabel": "{{count}} hinterher"
|
||||
"label": "Voraus / Hinterher",
|
||||
"ahead": "Voraus",
|
||||
"behind": "Hinterher"
|
||||
},
|
||||
"delete": {
|
||||
"button": "Löschen",
|
||||
|
||||
@@ -158,8 +158,9 @@
|
||||
"dangerZone": "Delete Branch",
|
||||
"aheadBehind": {
|
||||
"tooltip": "{{ahead}} commit(s) ahead, {{behind}} commit(s) behind default branch",
|
||||
"aheadLabel": "{{count}} ahead",
|
||||
"behindLabel": "{{count}} behind"
|
||||
"label": "Ahead / Behind",
|
||||
"ahead": "Ahead",
|
||||
"behind": "Behind"
|
||||
},
|
||||
"delete": {
|
||||
"button": "Delete",
|
||||
|
||||
@@ -26,92 +26,43 @@ import { Branch, BranchDetails } from "@scm-manager/ui-types";
|
||||
import React, { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import { Tooltip } from "@scm-manager/ui-components";
|
||||
import { calculateBarLength } from "./aheadBehind";
|
||||
import { Tooltip } from "@scm-manager/ui-overlays";
|
||||
import { Icon } from "@scm-manager/ui-buttons";
|
||||
|
||||
type Props = {
|
||||
branch: Branch;
|
||||
details: BranchDetails;
|
||||
hiddenMobile?: boolean;
|
||||
verbose?: boolean;
|
||||
details?: BranchDetails;
|
||||
labelId?: string
|
||||
};
|
||||
|
||||
type BarProps = {
|
||||
width: number;
|
||||
direction: "right" | "left";
|
||||
};
|
||||
|
||||
const Ahead = styled.span`
|
||||
border-left: 1px solid gray;
|
||||
`;
|
||||
|
||||
const Behind = styled.span``;
|
||||
|
||||
const Count = styled.span`
|
||||
word-break: keep-all;
|
||||
width: max-content;
|
||||
`;
|
||||
|
||||
const Bar = styled.span.attrs<BarProps>((props) => ({
|
||||
style: {
|
||||
width: props.width + "%",
|
||||
borderRadius: props.direction === "left" ? "25px 0 0 25px" : "0 25px 25px 0",
|
||||
},
|
||||
}))<BarProps>`
|
||||
height: 3px;
|
||||
max-width: 100%;
|
||||
margin-top: -2px;
|
||||
margin-bottom: 2px;
|
||||
`;
|
||||
|
||||
const TooltipWithDefaultCursor = styled(Tooltip)`
|
||||
cursor: default !important;
|
||||
`;
|
||||
|
||||
const AheadBehindTag: FC<Props> = ({ branch, details, hiddenMobile, verbose }) => {
|
||||
const AheadBehindTag: FC<Props> = ({ branch, details, labelId }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
|
||||
if (
|
||||
branch.defaultBranch ||
|
||||
!details ||
|
||||
typeof details.changesetsBehind !== "number" ||
|
||||
typeof details.changesetsAhead !== "number"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const behindText = verbose
|
||||
? t("branch.aheadBehind.behindLabel", { count: details.changesetsBehind })
|
||||
: details.changesetsBehind;
|
||||
const aheadText = verbose
|
||||
? t("branch.aheadBehind.aheadLabel", { count: details.changesetsAhead })
|
||||
: details.changesetsAhead;
|
||||
|
||||
return (
|
||||
<div className={`is-flex is-justify-content-center is-unselectable my-1 ${hiddenMobile ? "is-hidden-mobile" : ""}`}>
|
||||
<TooltipWithDefaultCursor
|
||||
message={t("branch.aheadBehind.tooltip", { ahead: details.changesetsAhead, behind: details.changesetsBehind })}
|
||||
location="top"
|
||||
>
|
||||
<div className="is-flex">
|
||||
<Behind className="is-flex is-flex-direction-column is-align-items-flex-end p-0">
|
||||
<Count className="is-size-7 pr-1">{behindText}</Count>
|
||||
<Bar
|
||||
className="has-rounded-border-left has-background-secondary"
|
||||
width={calculateBarLength(details.changesetsBehind)}
|
||||
direction="left"
|
||||
/>
|
||||
</Behind>
|
||||
<Ahead className="is-flex is-flex-direction-column is-align-items-flex-start p-0">
|
||||
<Count className="is-size-7 pl-1">{aheadText}</Count>
|
||||
<Bar
|
||||
className="has-rounded-border-right has-background-secondary"
|
||||
width={calculateBarLength(details.changesetsAhead)}
|
||||
direction="right"
|
||||
/>
|
||||
</Ahead>
|
||||
</div>
|
||||
</TooltipWithDefaultCursor>
|
||||
</div>
|
||||
<Tooltip
|
||||
message={t("branch.aheadBehind.tooltip", { ahead: details.changesetsAhead, behind: details.changesetsBehind })}
|
||||
>
|
||||
<span aria-labelledby={labelId} className="is-inline-flex is-align-items-center">
|
||||
<Icon className={details.changesetsAhead > 0 ? "has-text-success" : "has-text-grey-light"}>arrow-up</Icon>
|
||||
<Count aria-label={t("branch.aheadBehind.ahead")} className="is-size-7 pl-0">{details.changesetsAhead}</Count>
|
||||
<Icon className={details.changesetsBehind > 0 ? "has-text-warning" : "has-text-grey-light"}>arrow-down</Icon>
|
||||
<Count aria-label={t("branch.aheadBehind.behind")} className="is-size-7 pr-1">{details.changesetsBehind}</Count>
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import React, { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import classNames from "classnames";
|
||||
import { Branch, Repository } from "@scm-manager/ui-types";
|
||||
import { SmallLoadingSpinner, Subtitle } from "@scm-manager/ui-components";
|
||||
import {SmallLoadingSpinner, Subtitle, useGeneratedId} from "@scm-manager/ui-components";
|
||||
import BranchButtonGroup from "./BranchButtonGroup";
|
||||
import DefaultBranchTag from "./DefaultBranchTag";
|
||||
import AheadBehindTag from "./AheadBehindTag";
|
||||
@@ -41,12 +41,12 @@ type Props = {
|
||||
const BranchDetail: FC<Props> = ({ repository, branch }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
const { data, isLoading } = useBranchDetails(repository, branch);
|
||||
|
||||
const labelId = useGeneratedId();
|
||||
let aheadBehind;
|
||||
if (isLoading) {
|
||||
aheadBehind = <SmallLoadingSpinner />;
|
||||
} else if (data) {
|
||||
aheadBehind = <AheadBehindTag branch={branch} details={data} verbose={true} />;
|
||||
aheadBehind = <AheadBehindTag branch={branch} details={data} labelId={labelId} />;
|
||||
} else {
|
||||
aheadBehind = null;
|
||||
}
|
||||
@@ -77,7 +77,7 @@ const BranchDetail: FC<Props> = ({ repository, branch }) => {
|
||||
<BranchButtonGroup repository={repository} branch={branch} />
|
||||
</div>
|
||||
</div>
|
||||
{aheadBehind}
|
||||
<span id={labelId} className="is-size-7 has-text-secondary">{t("branch.aheadBehind.label")}</span>{aheadBehind}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -23,8 +23,7 @@
|
||||
*/
|
||||
|
||||
import { Branch, BranchDetails, Repository } from "@scm-manager/ui-types";
|
||||
import React, { FC, useMemo } from "react";
|
||||
import { extensionPoints } from "@scm-manager/ui-extensions";
|
||||
import React, { FC } from "react";
|
||||
import { CardList } from "@scm-manager/ui-layout";
|
||||
import { ErrorNotification } from "@scm-manager/ui-components";
|
||||
import { useDeleteBranch } from "@scm-manager/ui-api";
|
||||
@@ -39,7 +38,6 @@ type Props = {
|
||||
|
||||
const BranchList: FC<Props> = ({ repository, baseUrl, branches, branchesDetails }) => {
|
||||
const { isLoading, error, remove } = useDeleteBranch(repository);
|
||||
const defaultBranchDetails = useMemo<extensionPoints.BranchListDetail["type"][]>(() => [], []);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -49,12 +47,11 @@ const BranchList: FC<Props> = ({ repository, baseUrl, branches, branchesDetails
|
||||
<BranchListItem
|
||||
key={branch.name}
|
||||
branch={branch}
|
||||
defaultBranchDetails={defaultBranchDetails}
|
||||
remove={remove}
|
||||
isLoading={isLoading}
|
||||
baseUrl={baseUrl}
|
||||
repository={repository}
|
||||
branchDetails={branchesDetails?.find(({ branchName }) => branchName === branch.name)}
|
||||
branchesDetails={branchesDetails}
|
||||
/>
|
||||
))}
|
||||
</CardList>
|
||||
|
||||
@@ -24,53 +24,29 @@
|
||||
|
||||
import { Dialog, Menu } from "@scm-manager/ui-overlays";
|
||||
import { Icon } from "@scm-manager/ui-buttons";
|
||||
import { CardList } from "@scm-manager/ui-layout";
|
||||
import { CardList, Card } from "@scm-manager/ui-layout";
|
||||
import { Link } from "react-router-dom";
|
||||
import { encodePart } from "../../sources/components/content/FileLink";
|
||||
import { useKeyboardIteratorTarget } from "@scm-manager/ui-shortcuts";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { DateFromNow, useGeneratedId } from "@scm-manager/ui-components";
|
||||
import { extensionPoints, useBinder } from "@scm-manager/ui-extensions";
|
||||
import { DateFromNow } from "@scm-manager/ui-components";
|
||||
import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions";
|
||||
import React, { FC } from "react";
|
||||
import { Branch, BranchDetails, Repository } from "@scm-manager/ui-types";
|
||||
import styled from "styled-components";
|
||||
|
||||
const DetailsContainer = styled(CardList.Card.Row)`
|
||||
gap: 0.5rem 1rem;
|
||||
`;
|
||||
|
||||
const BranchDetail: FC<{
|
||||
branch: Branch;
|
||||
repository: Repository;
|
||||
detail: extensionPoints.BranchListDetail["type"];
|
||||
}> = ({ repository, detail, branch }) => {
|
||||
const labelId = useGeneratedId();
|
||||
const renderedDetail = detail.render({ branch, repository, labelId });
|
||||
if (!renderedDetail) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<span key={detail.name}>
|
||||
<span className="has-text-secondary mr-1" id={labelId}>
|
||||
{detail.name}
|
||||
</span>
|
||||
{renderedDetail}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
import AheadBehindTag from "./AheadBehindTag";
|
||||
|
||||
type Props = {
|
||||
branch: Branch;
|
||||
defaultBranchDetails: extensionPoints.BranchListDetail["type"][];
|
||||
remove: (branch: Branch) => void;
|
||||
isLoading: boolean;
|
||||
baseUrl: string;
|
||||
repository: Repository;
|
||||
branchDetails?: BranchDetails;
|
||||
branchesDetails?: BranchDetails[];
|
||||
};
|
||||
|
||||
const BranchListItem: FC<Props> = ({ branch, defaultBranchDetails, remove, isLoading, baseUrl, repository }) => {
|
||||
const binder = useBinder();
|
||||
|
||||
|
||||
const BranchListItem: FC<Props> = ({ branch, remove, isLoading, branchesDetails, baseUrl, repository }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
|
||||
return (
|
||||
@@ -98,14 +74,14 @@ const BranchListItem: FC<Props> = ({ branch, defaultBranchDetails, remove, isLoa
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<CardList.Card.Row className="is-flex">
|
||||
<CardList.Card.Title>
|
||||
<Card.Row className="is-flex">
|
||||
<Card.Title>
|
||||
<Link to={`${baseUrl}/${encodePart(branch.name)}/info`} ref={useKeyboardIteratorTarget()}>
|
||||
{branch.name}
|
||||
</Link>
|
||||
</CardList.Card.Title>
|
||||
</CardList.Card.Row>
|
||||
<CardList.Card.Row className="is-flex is-flex-wrap-wrap is-size-7 has-text-secondary is-white-space-pre">
|
||||
</Card.Title>
|
||||
</Card.Row>
|
||||
<Card.Row className="is-flex is-flex-wrap-wrap is-size-7 has-text-secondary is-white-space-pre">
|
||||
<Trans
|
||||
t={t}
|
||||
i18nKey="branches.table.branches.subtitle"
|
||||
@@ -117,18 +93,33 @@ const BranchListItem: FC<Props> = ({ branch, defaultBranchDetails, remove, isLoa
|
||||
space: <span />,
|
||||
}}
|
||||
/>
|
||||
</CardList.Card.Row>
|
||||
<DetailsContainer className="is-flex is-flex-wrap-wrap is-align-items-center">
|
||||
{[
|
||||
...defaultBranchDetails,
|
||||
...binder.getExtensions<extensionPoints.BranchListDetail>("branches.list.detail", {
|
||||
branch,
|
||||
repository,
|
||||
}),
|
||||
].map((detail) => (
|
||||
<BranchDetail key={detail.name} branch={branch} detail={detail} repository={repository} />
|
||||
))}
|
||||
</DetailsContainer>
|
||||
</Card.Row>
|
||||
<Card.Row>
|
||||
<Card.Details>
|
||||
<Card.Details.Detail>
|
||||
{({ labelId }) => (
|
||||
<>
|
||||
<Card.Details.Detail.Label id={labelId}>
|
||||
{branch.defaultBranch ? null : t("branch.aheadBehind.label")}
|
||||
</Card.Details.Detail.Label>
|
||||
<AheadBehindTag
|
||||
branch={branch}
|
||||
details={branchesDetails?.find(({ branchName }) => branchName === branch.name)}
|
||||
labelId={labelId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Card.Details.Detail>
|
||||
<ExtensionPoint<extensionPoints.BranchListDetail>
|
||||
name="branches.list.detail"
|
||||
props={{
|
||||
branch,
|
||||
repository,
|
||||
}}
|
||||
renderAll
|
||||
/>
|
||||
</Card.Details>
|
||||
</Card.Row>
|
||||
</CardList.Card>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -40,6 +40,10 @@ const BranchListWrapper = styled.div`
|
||||
gap: 1rem;
|
||||
`;
|
||||
|
||||
const HeaderWrapper = styled.div`
|
||||
gap: 0.5rem 1rem;
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
baseUrl: string;
|
||||
@@ -70,27 +74,25 @@ const BranchTableWrapper: FC<Props> = ({ repository, baseUrl, data }) => {
|
||||
<>
|
||||
<Subtitle subtitle={t("branches.overview.title")} />
|
||||
<ErrorNotification error={error} />
|
||||
<div className="is-flex mb-3 is-justify-content-space-between">
|
||||
<div>
|
||||
<div className="is-flex is-align-items-center mb-3">
|
||||
<label className="mr-2" htmlFor="branches-overview-sort">
|
||||
{t("branches.overview.sort.label")}
|
||||
</label>
|
||||
<Select id="branches-overview-sort" onChange={(e) => setSort(e.target.value as SortOption)}>
|
||||
{SORT_OPTIONS.map((sortOption) => (
|
||||
<option key={sortOption} value={sortOption}>
|
||||
{t(`branches.overview.sort.option.${sortOption}`)}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<HeaderWrapper className="is-flex is-flex-wrap-wrap is-justify-content-space-between mb-3">
|
||||
<div className="is-flex is-align-items-center">
|
||||
<label className="mr-2" htmlFor="branches-overview-sort">
|
||||
{t("branches.overview.sort.label")}
|
||||
</label>
|
||||
<Select id="branches-overview-sort" onChange={(e) => setSort(e.target.value as SortOption)}>
|
||||
{SORT_OPTIONS.map((sortOption) => (
|
||||
<option key={sortOption} value={sortOption}>
|
||||
{t(`branches.overview.sort.option.${sortOption}`)}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
{showCreateButton ? (
|
||||
<LinkButton variant="primary" to="./create">
|
||||
{t("branches.overview.createButton")}
|
||||
</LinkButton>
|
||||
) : null}
|
||||
</div>
|
||||
</HeaderWrapper>
|
||||
<BranchListWrapper className="is-flex is-flex-direction-column">
|
||||
<KeyboardIterator>
|
||||
{activeBranches.length > 0 ? (
|
||||
|
||||
Reference in New Issue
Block a user