mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-13 00:45:44 +01:00
Create atomic design page template for master-detail views
This commit is contained in:
2
gradle/changelog/data_page_template.yaml
Normal file
2
gradle/changelog/data_page_template.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
- type: added
|
||||||
|
description: Atomic design page template simple data pages
|
||||||
@@ -27,7 +27,7 @@ import classNames from "classnames";
|
|||||||
type NotificationType = "primary" | "info" | "success" | "warning" | "danger" | "inherit";
|
type NotificationType = "primary" | "info" | "success" | "warning" | "danger" | "inherit";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type: NotificationType;
|
type?: NotificationType;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@
|
|||||||
"@scm-manager/tsconfig": "^2.13.0",
|
"@scm-manager/tsconfig": "^2.13.0",
|
||||||
"@scm-manager/ui-styles": "2.46.2-SNAPSHOT",
|
"@scm-manager/ui-styles": "2.46.2-SNAPSHOT",
|
||||||
"@scm-manager/ui-overlays": "2.46.2-SNAPSHOT",
|
"@scm-manager/ui-overlays": "2.46.2-SNAPSHOT",
|
||||||
|
"@scm-manager/ui-forms": "2.46.2-SNAPSHOT",
|
||||||
"@storybook/addon-actions": "^6.5.10",
|
"@storybook/addon-actions": "^6.5.10",
|
||||||
"@storybook/addon-docs": "^6.5.14",
|
"@storybook/addon-docs": "^6.5.14",
|
||||||
"@storybook/addon-essentials": "^6.5.10",
|
"@storybook/addon-essentials": "^6.5.10",
|
||||||
@@ -48,6 +49,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-collapsible": "^1.0.3",
|
"@radix-ui/react-collapsible": "^1.0.3",
|
||||||
|
"@radix-ui/react-slot": "^1.0.1",
|
||||||
"@scm-manager/ui-buttons": "2.46.2-SNAPSHOT"
|
"@scm-manager/ui-buttons": "2.46.2-SNAPSHOT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
52
scm-ui/ui-layout/src/_helpers/with-classes.tsx
Normal file
52
scm-ui/ui-layout/src/_helpers/with-classes.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* 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, { ComponentProps, ElementRef, ForwardRefExoticComponent, ReactElement } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
|
|
||||||
|
const withClasses = <Element extends React.ElementType | ForwardRefExoticComponent<any>>(
|
||||||
|
typ: Element,
|
||||||
|
additionalClassNames: string[],
|
||||||
|
additionalProps?: Partial<ComponentProps<Element>>
|
||||||
|
) =>
|
||||||
|
React.forwardRef<
|
||||||
|
ElementRef<Element>,
|
||||||
|
| (ComponentProps<Element> & { asChild?: false; className?: string })
|
||||||
|
| { asChild: true; children: ReactElement<{ className?: string }> }
|
||||||
|
>((props, ref) => {
|
||||||
|
// @ts-ignore Typescript doesn't get it for some reason
|
||||||
|
if (props.asChild) {
|
||||||
|
return <Slot {...props} className={classNames(...additionalClassNames)} />;
|
||||||
|
} else {
|
||||||
|
return React.createElement(typ, {
|
||||||
|
...additionalProps,
|
||||||
|
...props,
|
||||||
|
className: classNames((props as any).className, ...additionalClassNames),
|
||||||
|
ref,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default withClasses;
|
||||||
@@ -59,7 +59,7 @@ const Card = React.forwardRef<HTMLElement, Props>(
|
|||||||
},
|
},
|
||||||
avatar ? avatar : null,
|
avatar ? avatar : null,
|
||||||
<div
|
<div
|
||||||
className="is-flex is-flex-direction-column is-justify-content-center is-flex-grow-1 is-clipped"
|
className="is-flex is-flex-direction-column is-justify-content-center is-flex-grow-1 is-overflow-wrap-anywhere"
|
||||||
style={{ gap: rowGap }}
|
style={{ gap: rowGap }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const CardDetail = React.forwardRef<HTMLSpanElement, CardDetailProps>(
|
|||||||
({ children, className, ...props }, ref) => {
|
({ children, className, ...props }, ref) => {
|
||||||
const labelId = useGeneratedId();
|
const labelId = useGeneratedId();
|
||||||
return (
|
return (
|
||||||
<span {...props} className={classNames("is-flex is-align-items-center", className)} ref={ref}>
|
<span {...props} className={classNames("is-flex is-align-items-center has-gap-1", className)} ref={ref}>
|
||||||
{typeof children === "function" ? children({ labelId }) : children}
|
{typeof children === "function" ? children({ labelId }) : children}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@@ -52,7 +52,7 @@ export const CardDetail = React.forwardRef<HTMLSpanElement, CardDetailProps>(
|
|||||||
*/
|
*/
|
||||||
export const CardDetailLabel = React.forwardRef<HTMLSpanElement, HTMLAttributes<HTMLSpanElement>>(
|
export const CardDetailLabel = React.forwardRef<HTMLSpanElement, HTMLAttributes<HTMLSpanElement>>(
|
||||||
({ children, className, ...props }, ref) => (
|
({ children, className, ...props }, ref) => (
|
||||||
<span {...props} className={classNames("has-text-secondary is-size-7 mr-1", className)} ref={ref}>
|
<span {...props} className={classNames("has-text-secondary is-size-7", className)} ref={ref}>
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,6 +22,9 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import React, { ComponentProps } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @beta
|
* @beta
|
||||||
* @since 2.44.0
|
* @since 2.44.0
|
||||||
@@ -29,3 +32,13 @@
|
|||||||
const CardRow = "div" as const;
|
const CardRow = "div" as const;
|
||||||
|
|
||||||
export default CardRow;
|
export default CardRow;
|
||||||
|
|
||||||
|
export const SecondaryRow = React.forwardRef<HTMLDivElement, ComponentProps<typeof CardRow>>(
|
||||||
|
({ className, ...props }, ref) => <CardRow className={classNames(className, "is-size-7")} {...props} ref={ref} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export const TertiaryRow = React.forwardRef<HTMLDivElement, ComponentProps<typeof CardRow>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<CardRow className={classNames(className, "is-size-7", "has-text-secondary")} {...props} ref={ref} />
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|||||||
@@ -24,15 +24,25 @@
|
|||||||
|
|
||||||
import CardListComponent, { CardListBox as CardListBoxComponent, CardListCard } from "./card-list/CardList";
|
import CardListComponent, { CardListBox as CardListBoxComponent, CardListCard } from "./card-list/CardList";
|
||||||
import CardTitle from "./card/CardTitle";
|
import CardTitle from "./card/CardTitle";
|
||||||
import CardRow from "./card/CardRow";
|
import CardRow, { SecondaryRow, TertiaryRow } from "./card/CardRow";
|
||||||
import { CardDetail, CardDetailLabel, CardDetails, CardDetailTag } from "./card/CardDetail";
|
import { CardDetail, CardDetailLabel, CardDetails, CardDetailTag } from "./card/CardDetail";
|
||||||
import CardComponent from "./card/Card";
|
import CardComponent from "./card/Card";
|
||||||
|
import {
|
||||||
|
DataPageHeader as DataPageHeaderComponent,
|
||||||
|
DataPageHeaderCreateButton,
|
||||||
|
DataPageHeaderSetting,
|
||||||
|
DataPageHeaderSettingField,
|
||||||
|
DataPageHeaderSettingLabel,
|
||||||
|
DataPageHeaderSettings,
|
||||||
|
} from "./templates/data-page/DataPageHeader";
|
||||||
|
|
||||||
export { default as Collapsible } from "./collapsible/Collapsible";
|
export { default as Collapsible } from "./collapsible/Collapsible";
|
||||||
|
|
||||||
const CardExport = {
|
const CardExport = {
|
||||||
Title: CardTitle,
|
Title: CardTitle,
|
||||||
Row: CardRow,
|
Row: CardRow,
|
||||||
|
SecondaryRow: SecondaryRow,
|
||||||
|
TertiaryRow: TertiaryRow,
|
||||||
Details: Object.assign(CardDetails, {
|
Details: Object.assign(CardDetails, {
|
||||||
Detail: Object.assign(CardDetail, {
|
Detail: Object.assign(CardDetail, {
|
||||||
Label: CardDetailLabel,
|
Label: CardDetailLabel,
|
||||||
@@ -49,3 +59,13 @@ const CardListExport = {
|
|||||||
|
|
||||||
export const CardList = Object.assign(CardListComponent, CardListExport);
|
export const CardList = Object.assign(CardListComponent, CardListExport);
|
||||||
export const CardListBox = Object.assign(CardListBoxComponent, CardListExport);
|
export const CardListBox = Object.assign(CardListBoxComponent, CardListExport);
|
||||||
|
|
||||||
|
export const DataPageHeader = Object.assign(DataPageHeaderComponent, {
|
||||||
|
Settings: Object.assign(DataPageHeaderSettings, {
|
||||||
|
Setting: Object.assign(DataPageHeaderSetting, {
|
||||||
|
Label: DataPageHeaderSettingLabel,
|
||||||
|
Field: DataPageHeaderSettingField,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
CreateButton: DataPageHeaderCreateButton,
|
||||||
|
});
|
||||||
|
|||||||
200
scm-ui/ui-layout/src/templates/data-page/DataPage.stories.tsx
Normal file
200
scm-ui/ui-layout/src/templates/data-page/DataPage.stories.tsx
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
/*
|
||||||
|
* 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 from "react";
|
||||||
|
import {
|
||||||
|
DataPageHeader,
|
||||||
|
DataPageHeaderCreateButton,
|
||||||
|
DataPageHeaderSetting,
|
||||||
|
DataPageHeaderSettingField,
|
||||||
|
DataPageHeaderSettingLabel,
|
||||||
|
DataPageHeaderSettings,
|
||||||
|
} from "./DataPageHeader";
|
||||||
|
import { Select } from "@scm-manager/ui-forms";
|
||||||
|
import { ComponentMeta, ComponentStory } from "@storybook/react";
|
||||||
|
import { ErrorNotification, Loading, Subtitle, Title, Notification } from "@scm-manager/ui-components";
|
||||||
|
import { Button, Icon } from "@scm-manager/ui-buttons";
|
||||||
|
import { CardListBox, CardListCard } from "../../card-list/CardList";
|
||||||
|
import CardRow, { SecondaryRow, TertiaryRow } from "../../card/CardRow";
|
||||||
|
import CardTitle from "../../card/CardTitle";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import StoryRouter from "storybook-react-router";
|
||||||
|
import { CardDetail, CardDetailLabel, CardDetails, CardDetailTag } from "../../card/CardDetail";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Data Page Template",
|
||||||
|
component: DataPageHeader,
|
||||||
|
decorators: [StoryRouter()],
|
||||||
|
} as ComponentMeta<typeof DataPageHeader>;
|
||||||
|
|
||||||
|
// @ts-ignore Storybook is not cooperating
|
||||||
|
export const Example: ComponentStory<{ error: Error; isLoading: boolean; isEmpty: boolean }> = ({
|
||||||
|
error,
|
||||||
|
isLoading,
|
||||||
|
isEmpty,
|
||||||
|
}: any) => {
|
||||||
|
let content;
|
||||||
|
if (error) {
|
||||||
|
content = <ErrorNotification error={error} />;
|
||||||
|
} else if (isLoading) {
|
||||||
|
content = <Loading />;
|
||||||
|
} else if (isEmpty) {
|
||||||
|
content = <Notification type="info">There is no data, consider adjusting the filters</Notification>;
|
||||||
|
} else {
|
||||||
|
content = (
|
||||||
|
<CardListBox>
|
||||||
|
<CardListCard avatar={<Icon>trash</Icon>} action={<Icon>ellipsis-v</Icon>}>
|
||||||
|
<CardRow>
|
||||||
|
<CardTitle>
|
||||||
|
<Link to="/item">
|
||||||
|
The title may contain a link but most importantly does not contain any information except the "display
|
||||||
|
name" of the entity. It is also text-only
|
||||||
|
</Link>
|
||||||
|
</CardTitle>
|
||||||
|
</CardRow>
|
||||||
|
<SecondaryRow>
|
||||||
|
This contains more important details about the card, but not quite as important as the title.
|
||||||
|
</SecondaryRow>
|
||||||
|
<TertiaryRow>This contains less important information about the card</TertiaryRow>
|
||||||
|
<CardRow>
|
||||||
|
<CardDetails>
|
||||||
|
<CardDetail>
|
||||||
|
<CardDetailLabel>Tags are great for numbers.</CardDetailLabel>
|
||||||
|
<CardDetailTag>7/3</CardDetailTag>
|
||||||
|
</CardDetail>
|
||||||
|
<CardDetail>
|
||||||
|
{({ labelId }) => (
|
||||||
|
<>
|
||||||
|
<CardDetailLabel id={labelId}>
|
||||||
|
Interactive details need 'is-relative' and 'aria-labelledby'
|
||||||
|
</CardDetailLabel>
|
||||||
|
<Button aria-labelledby={labelId} className="is-relative has-background-transparent is-borderless">
|
||||||
|
<Icon>edit</Icon>
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardDetail>
|
||||||
|
</CardDetails>
|
||||||
|
</CardRow>
|
||||||
|
</CardListCard>
|
||||||
|
<CardListCard avatar={<Icon>users</Icon>} action={<Icon>ellipsis-v</Icon>}>
|
||||||
|
<CardRow>
|
||||||
|
<CardTitle>
|
||||||
|
<Link to="/item">
|
||||||
|
We can also enter insane text without whitespace
|
||||||
|
ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss
|
||||||
|
</Link>
|
||||||
|
</CardTitle>
|
||||||
|
</CardRow>
|
||||||
|
<SecondaryRow>
|
||||||
|
SCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCM
|
||||||
|
</SecondaryRow>
|
||||||
|
<TertiaryRow>
|
||||||
|
SCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCM
|
||||||
|
</TertiaryRow>
|
||||||
|
<CardRow>
|
||||||
|
<CardDetails>
|
||||||
|
<CardDetail>
|
||||||
|
<CardDetailLabel>SCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCM</CardDetailLabel>
|
||||||
|
<CardDetailTag>7/3</CardDetailTag>
|
||||||
|
</CardDetail>
|
||||||
|
<CardDetail>
|
||||||
|
{({ labelId }) => (
|
||||||
|
<>
|
||||||
|
<CardDetailLabel id={labelId}>SCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCM</CardDetailLabel>
|
||||||
|
<Button aria-labelledby={labelId} className="is-relative has-background-transparent is-borderless">
|
||||||
|
<Icon>trash</Icon>
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardDetail>
|
||||||
|
</CardDetails>
|
||||||
|
</CardRow>
|
||||||
|
</CardListCard>
|
||||||
|
</CardListBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Title>My Page</Title>
|
||||||
|
<Subtitle subtitle="All the data" />
|
||||||
|
<DataPageHeader>
|
||||||
|
<DataPageHeaderSettings>
|
||||||
|
<DataPageHeaderSetting>
|
||||||
|
{({ formFieldId }) => (
|
||||||
|
<>
|
||||||
|
<DataPageHeaderSettingLabel htmlFor={formFieldId}>Filter by</DataPageHeaderSettingLabel>
|
||||||
|
<DataPageHeaderSettingField>
|
||||||
|
<Select
|
||||||
|
id={formFieldId}
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: "Yes",
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "No",
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</DataPageHeaderSettingField>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DataPageHeaderSetting>
|
||||||
|
<DataPageHeaderSetting>
|
||||||
|
{({ formFieldId }) => (
|
||||||
|
<>
|
||||||
|
<DataPageHeaderSettingLabel htmlFor={formFieldId}>Sort by</DataPageHeaderSettingLabel>
|
||||||
|
<DataPageHeaderSettingField>
|
||||||
|
<Select
|
||||||
|
id={formFieldId}
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: "Blue",
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Red",
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</DataPageHeaderSettingField>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DataPageHeaderSetting>
|
||||||
|
</DataPageHeaderSettings>
|
||||||
|
<DataPageHeaderCreateButton to="/mydata/create">Create New Data</DataPageHeaderCreateButton>
|
||||||
|
</DataPageHeader>
|
||||||
|
{content}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
Example.args = {
|
||||||
|
error: undefined,
|
||||||
|
isLoading: false,
|
||||||
|
isEmpty: false,
|
||||||
|
};
|
||||||
100
scm-ui/ui-layout/src/templates/data-page/DataPageHeader.tsx
Normal file
100
scm-ui/ui-layout/src/templates/data-page/DataPageHeader.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
* 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 withClasses from "../../_helpers/with-classes";
|
||||||
|
import React, { HTMLAttributes } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { useGeneratedId } from "@scm-manager/ui-components";
|
||||||
|
import { LinkButton } from "@scm-manager/ui-buttons";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @beta
|
||||||
|
* @since 2.47.0
|
||||||
|
*/
|
||||||
|
export const DataPageHeader = withClasses("div", [
|
||||||
|
"is-flex",
|
||||||
|
"is-flex-wrap-wrap",
|
||||||
|
"is-justify-content-space-between",
|
||||||
|
"mb-3",
|
||||||
|
"has-row-gap-2",
|
||||||
|
"has-column-gap-4",
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @beta
|
||||||
|
* @since 2.47.0
|
||||||
|
*/
|
||||||
|
export const DataPageHeaderSettings = withClasses("div", [
|
||||||
|
"is-flex",
|
||||||
|
"is-flex-wrap-wrap",
|
||||||
|
"is-align-items-center",
|
||||||
|
"has-row-gap-2",
|
||||||
|
"has-column-gap-4",
|
||||||
|
"is-flex-grow-1",
|
||||||
|
"is-flex-shrink-1",
|
||||||
|
"is-flex-basis-0",
|
||||||
|
]);
|
||||||
|
|
||||||
|
type DataPageHeaderSettingProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
children?: React.ReactNode | ((props: { formFieldId: string }) => React.ReactNode);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @beta
|
||||||
|
* @since 2.47.0
|
||||||
|
*/
|
||||||
|
export const DataPageHeaderSetting = React.forwardRef<HTMLSpanElement, DataPageHeaderSettingProps>(
|
||||||
|
({ className, children, ...props }, ref) => {
|
||||||
|
const formFieldId = useGeneratedId();
|
||||||
|
return (
|
||||||
|
<span {...props} className={classNames(className, "is-flex", "is-align-items-center", "has-gap-2")} ref={ref}>
|
||||||
|
{typeof children === "function" ? children({ formFieldId }) : children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @beta
|
||||||
|
* @since 2.47.0
|
||||||
|
*/
|
||||||
|
export const DataPageHeaderSettingLabel = withClasses("label", [
|
||||||
|
"is-flex",
|
||||||
|
"is-align-items-center",
|
||||||
|
"is-text-wrap-no-wrap",
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @beta
|
||||||
|
* @since 2.47.0
|
||||||
|
*/
|
||||||
|
export const DataPageHeaderSettingField = React.Fragment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @beta
|
||||||
|
* @since 2.47.0
|
||||||
|
*/
|
||||||
|
export const DataPageHeaderCreateButton = withClasses(LinkButton, ["is-flex-grow-0", "is-flex-shrink-0"], {
|
||||||
|
variant: "primary",
|
||||||
|
});
|
||||||
29
scm-ui/ui-styles/src/components/_flex.scss
Normal file
29
scm-ui/ui-styles/src/components/_flex.scss
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@each $size, $value in $spacing-values {
|
||||||
|
.is-flex-basis-#{$size} {
|
||||||
|
flex-basis: $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
scm-ui/ui-styles/src/components/_gap.scss
Normal file
35
scm-ui/ui-styles/src/components/_gap.scss
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@each $size, $value in $spacing-values {
|
||||||
|
.has-gap-#{$size} {
|
||||||
|
gap: $value;
|
||||||
|
}
|
||||||
|
.has-row-gap-#{$size} {
|
||||||
|
row-gap: $value;
|
||||||
|
}
|
||||||
|
.has-column-gap-#{$size} {
|
||||||
|
column-gap: $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,7 +53,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.is-overflow-wrap-anywhere {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-text-wrap-no-wrap {
|
||||||
|
text-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.is-absolute {
|
.is-absolute {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -25,5 +25,7 @@
|
|||||||
@import "../variables/_derived.scss";
|
@import "../variables/_derived.scss";
|
||||||
@import "bulma-popover/css/bulma-popover";
|
@import "bulma-popover/css/bulma-popover";
|
||||||
@import "../components/_main.scss";
|
@import "../components/_main.scss";
|
||||||
|
@import "../components/_gap.scss";
|
||||||
|
@import "../components/_flex.scss";
|
||||||
@import "../components/_tooltip.scss";
|
@import "../components/_tooltip.scss";
|
||||||
@import "../components/_card.scss";
|
@import "../components/_card.scss";
|
||||||
|
|||||||
@@ -30,19 +30,9 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useBranchDetailsCollection } from "@scm-manager/ui-api";
|
import { useBranchDetailsCollection } from "@scm-manager/ui-api";
|
||||||
import { KeyboardIterator } from "@scm-manager/ui-shortcuts";
|
import { KeyboardIterator } from "@scm-manager/ui-shortcuts";
|
||||||
import BranchList from "../components/BranchList";
|
import BranchList from "../components/BranchList";
|
||||||
import { Collapsible } from "@scm-manager/ui-layout";
|
import { Collapsible, DataPageHeader } from "@scm-manager/ui-layout";
|
||||||
import { LinkButton } from "@scm-manager/ui-buttons";
|
|
||||||
import { Select } from "@scm-manager/ui-forms";
|
import { Select } from "@scm-manager/ui-forms";
|
||||||
import { SORT_OPTIONS, SortOption } from "../../tags/orderTags";
|
import { SORT_OPTIONS, SortOption } from "../../tags/orderTags";
|
||||||
import styled from "styled-components";
|
|
||||||
|
|
||||||
const BranchListWrapper = styled.div`
|
|
||||||
gap: 1rem;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const HeaderWrapper = styled.div`
|
|
||||||
gap: 0.5rem 1rem;
|
|
||||||
`;
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
@@ -74,26 +64,32 @@ const BranchTableWrapper: FC<Props> = ({ repository, baseUrl, data }) => {
|
|||||||
<>
|
<>
|
||||||
<Subtitle subtitle={t("branches.overview.title")} />
|
<Subtitle subtitle={t("branches.overview.title")} />
|
||||||
<ErrorNotification error={error} />
|
<ErrorNotification error={error} />
|
||||||
<HeaderWrapper className="is-flex is-flex-wrap-wrap is-justify-content-space-between mb-3">
|
<DataPageHeader>
|
||||||
<div className="is-flex is-align-items-center">
|
<DataPageHeader.Settings className="is-flex is-align-items-center">
|
||||||
<label className="mr-2" htmlFor="branches-overview-sort">
|
<DataPageHeader.Settings.Setting>
|
||||||
{t("branches.overview.sort.label")}
|
{({ formFieldId }) => (
|
||||||
</label>
|
<>
|
||||||
<Select id="branches-overview-sort" onChange={(e) => setSort(e.target.value as SortOption)}>
|
<DataPageHeader.Settings.Setting.Label htmlFor={formFieldId}>
|
||||||
{SORT_OPTIONS.map((sortOption) => (
|
{t("branches.overview.sort.label")}
|
||||||
<option key={sortOption} value={sortOption}>
|
</DataPageHeader.Settings.Setting.Label>
|
||||||
{t(`branches.overview.sort.option.${sortOption}`)}
|
<DataPageHeader.Settings.Setting.Field>
|
||||||
</option>
|
<Select id={formFieldId} onChange={(e) => setSort(e.target.value as SortOption)}>
|
||||||
))}
|
{SORT_OPTIONS.map((sortOption) => (
|
||||||
</Select>
|
<option key={sortOption} value={sortOption}>
|
||||||
</div>
|
{t(`branches.overview.sort.option.${sortOption}`)}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</DataPageHeader.Settings.Setting.Field>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DataPageHeader.Settings.Setting>
|
||||||
|
</DataPageHeader.Settings>
|
||||||
{showCreateButton ? (
|
{showCreateButton ? (
|
||||||
<LinkButton variant="primary" to="./create">
|
<DataPageHeader.CreateButton to="./create">{t("branches.overview.createButton")}</DataPageHeader.CreateButton>
|
||||||
{t("branches.overview.createButton")}
|
|
||||||
</LinkButton>
|
|
||||||
) : null}
|
) : null}
|
||||||
</HeaderWrapper>
|
</DataPageHeader>
|
||||||
<BranchListWrapper className="is-flex is-flex-direction-column">
|
<div className="is-flex is-flex-direction-column has-gap-4">
|
||||||
<KeyboardIterator>
|
<KeyboardIterator>
|
||||||
{activeBranches.length > 0 ? (
|
{activeBranches.length > 0 ? (
|
||||||
<Collapsible header={t("branches.table.branches.active")}>
|
<Collapsible header={t("branches.table.branches.active")}>
|
||||||
@@ -116,7 +112,7 @@ const BranchTableWrapper: FC<Props> = ({ repository, baseUrl, data }) => {
|
|||||||
</Collapsible>
|
</Collapsible>
|
||||||
) : null}
|
) : null}
|
||||||
</KeyboardIterator>
|
</KeyboardIterator>
|
||||||
</BranchListWrapper>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user