{children}
diff --git a/scm-ui/ui-layout/src/card/CardDetail.tsx b/scm-ui/ui-layout/src/card/CardDetail.tsx
index 7e4247bf23..4c051fb49f 100644
--- a/scm-ui/ui-layout/src/card/CardDetail.tsx
+++ b/scm-ui/ui-layout/src/card/CardDetail.tsx
@@ -39,7 +39,7 @@ export const CardDetail = React.forwardRef(
({ children, className, ...props }, ref) => {
const labelId = useGeneratedId();
return (
-
+
{typeof children === "function" ? children({ labelId }) : children}
);
@@ -52,7 +52,7 @@ export const CardDetail = React.forwardRef(
*/
export const CardDetailLabel = React.forwardRef>(
({ children, className, ...props }, ref) => (
-
+
{children}
)
diff --git a/scm-ui/ui-layout/src/card/CardRow.tsx b/scm-ui/ui-layout/src/card/CardRow.tsx
index d7009e5f70..9613ac8436 100644
--- a/scm-ui/ui-layout/src/card/CardRow.tsx
+++ b/scm-ui/ui-layout/src/card/CardRow.tsx
@@ -22,6 +22,9 @@
* SOFTWARE.
*/
+import React, { ComponentProps } from "react";
+import classNames from "classnames";
+
/**
* @beta
* @since 2.44.0
@@ -29,3 +32,13 @@
const CardRow = "div" as const;
export default CardRow;
+
+export const SecondaryRow = React.forwardRef>(
+ ({ className, ...props }, ref) =>
+);
+
+export const TertiaryRow = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+);
diff --git a/scm-ui/ui-layout/src/index.ts b/scm-ui/ui-layout/src/index.ts
index f867b3a46c..1d8cf27cd2 100644
--- a/scm-ui/ui-layout/src/index.ts
+++ b/scm-ui/ui-layout/src/index.ts
@@ -24,15 +24,25 @@
import CardListComponent, { CardListBox as CardListBoxComponent, CardListCard } from "./card-list/CardList";
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 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";
const CardExport = {
Title: CardTitle,
Row: CardRow,
+ SecondaryRow: SecondaryRow,
+ TertiaryRow: TertiaryRow,
Details: Object.assign(CardDetails, {
Detail: Object.assign(CardDetail, {
Label: CardDetailLabel,
@@ -49,3 +59,13 @@ const CardListExport = {
export const CardList = Object.assign(CardListComponent, 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,
+});
diff --git a/scm-ui/ui-layout/src/templates/data-page/DataPage.stories.tsx b/scm-ui/ui-layout/src/templates/data-page/DataPage.stories.tsx
new file mode 100644
index 0000000000..f5411f1de3
--- /dev/null
+++ b/scm-ui/ui-layout/src/templates/data-page/DataPage.stories.tsx
@@ -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;
+
+// @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 = ;
+ } else if (isLoading) {
+ content = ;
+ } else if (isEmpty) {
+ content = There is no data, consider adjusting the filters;
+ } else {
+ content = (
+
+ trash} action={ellipsis-v}>
+
+
+
+ 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
+
+
+
+
+ This contains more important details about the card, but not quite as important as the title.
+
+ This contains less important information about the card
+
+
+
+ Tags are great for numbers.
+ 7/3
+
+
+ {({ labelId }) => (
+ <>
+
+ Interactive details need 'is-relative' and 'aria-labelledby'
+
+
+ >
+ )}
+
+
+
+
+ users} action={ellipsis-v}>
+
+
+
+ We can also enter insane text without whitespace
+ ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss
+
+
+
+
+ SCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCM
+
+
+ SCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCM
+
+
+
+
+ SCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCM
+ 7/3
+
+
+ {({ labelId }) => (
+ <>
+ SCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCMSCM
+
+ >
+ )}
+
+
+
+
+
+ );
+ }
+
+ return (
+ <>
+ My Page
+
+
+
+
+ {({ formFieldId }) => (
+ <>
+ Filter by
+
+
+
+ >
+ )}
+
+
+ {({ formFieldId }) => (
+ <>
+ Sort by
+
+
+
+ >
+ )}
+
+
+ Create New Data
+
+ {content}
+ >
+ );
+};
+Example.args = {
+ error: undefined,
+ isLoading: false,
+ isEmpty: false,
+};
diff --git a/scm-ui/ui-layout/src/templates/data-page/DataPageHeader.tsx b/scm-ui/ui-layout/src/templates/data-page/DataPageHeader.tsx
new file mode 100644
index 0000000000..dcd87900b0
--- /dev/null
+++ b/scm-ui/ui-layout/src/templates/data-page/DataPageHeader.tsx
@@ -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 & {
+ children?: React.ReactNode | ((props: { formFieldId: string }) => React.ReactNode);
+};
+
+/**
+ * @beta
+ * @since 2.47.0
+ */
+export const DataPageHeaderSetting = React.forwardRef(
+ ({ className, children, ...props }, ref) => {
+ const formFieldId = useGeneratedId();
+ return (
+
+ {typeof children === "function" ? children({ formFieldId }) : children}
+
+ );
+ }
+);
+
+/**
+ * @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",
+});
diff --git a/scm-ui/ui-styles/src/components/_flex.scss b/scm-ui/ui-styles/src/components/_flex.scss
new file mode 100644
index 0000000000..4985c739cc
--- /dev/null
+++ b/scm-ui/ui-styles/src/components/_flex.scss
@@ -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;
+ }
+}
diff --git a/scm-ui/ui-styles/src/components/_gap.scss b/scm-ui/ui-styles/src/components/_gap.scss
new file mode 100644
index 0000000000..3d722a311f
--- /dev/null
+++ b/scm-ui/ui-styles/src/components/_gap.scss
@@ -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;
+ }
+}
diff --git a/scm-ui/ui-styles/src/components/_main.scss b/scm-ui/ui-styles/src/components/_main.scss
index 351b4bfa63..a62a03bdb2 100644
--- a/scm-ui/ui-styles/src/components/_main.scss
+++ b/scm-ui/ui-styles/src/components/_main.scss
@@ -53,7 +53,13 @@
}
}
+.is-overflow-wrap-anywhere {
+ overflow-wrap: anywhere;
+}
+.is-text-wrap-no-wrap {
+ text-wrap: nowrap;
+}
.is-absolute {
position: absolute;
diff --git a/scm-ui/ui-styles/src/utils/_post.scss b/scm-ui/ui-styles/src/utils/_post.scss
index f132a914c6..42d1d2cb45 100644
--- a/scm-ui/ui-styles/src/utils/_post.scss
+++ b/scm-ui/ui-styles/src/utils/_post.scss
@@ -25,5 +25,7 @@
@import "../variables/_derived.scss";
@import "bulma-popover/css/bulma-popover";
@import "../components/_main.scss";
+@import "../components/_gap.scss";
+@import "../components/_flex.scss";
@import "../components/_tooltip.scss";
@import "../components/_card.scss";
diff --git a/scm-ui/ui-webapp/src/repos/branches/containers/BranchTableWrapper.tsx b/scm-ui/ui-webapp/src/repos/branches/containers/BranchTableWrapper.tsx
index f3ea6683eb..428d6832b0 100644
--- a/scm-ui/ui-webapp/src/repos/branches/containers/BranchTableWrapper.tsx
+++ b/scm-ui/ui-webapp/src/repos/branches/containers/BranchTableWrapper.tsx
@@ -30,19 +30,9 @@ import { useTranslation } from "react-i18next";
import { useBranchDetailsCollection } from "@scm-manager/ui-api";
import { KeyboardIterator } from "@scm-manager/ui-shortcuts";
import BranchList from "../components/BranchList";
-import { Collapsible } from "@scm-manager/ui-layout";
-import { LinkButton } from "@scm-manager/ui-buttons";
+import { Collapsible, DataPageHeader } from "@scm-manager/ui-layout";
import { Select } from "@scm-manager/ui-forms";
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 = {
repository: Repository;
@@ -74,26 +64,32 @@ const BranchTableWrapper: FC = ({ repository, baseUrl, data }) => {
<>
-
-