= {
props: P;
};
-export type SimpleDynamicExtensionPointDefinition = ExtensionPointDefinition;
+export type SimpleDynamicExtensionPointDefinition
=
+ ExtensionPointDefinition;
+
+export type BindOptions = {
+ predicate?: Predicate;
+
+ /**
+ * Extensions are ordered by name (ASC).
+ */
+ extensionName?: string;
+
+ /**
+ * Extensions are ordered by priority (DESC).
+ */
+ priority?: number;
+};
+
+function isBindOptions(input?: Predicate | BindOptions): input is BindOptions {
+ return typeof input !== "function" && typeof input === "object";
+}
/**
* Binder is responsible for binding plugin extensions to their corresponding extension points.
@@ -60,10 +80,7 @@ export class Binder {
* @param extension provided extension
* @param predicate to decide if the extension gets rendered for the given props
*/
- bind>(
- extensionPoint: E["name"],
- extension: E["type"]
- ): void;
+ bind>(extensionPoint: E["name"], extension: E["type"]): void;
bind>(
extensionPoint: E["name"],
extension: E["type"],
@@ -73,17 +90,38 @@ export class Binder {
bind>(
extensionPoint: E["name"],
extension: E["type"],
- predicate?: Predicate,
+ options?: BindOptions
+ ): void;
+ bind>(
+ extensionPoint: E["name"],
+ extension: E["type"],
+ predicateOrOptions?: Predicate | BindOptions,
extensionName?: string
) {
+ let predicate: Predicate = () => true;
+ let priority = 0;
+ if (isBindOptions(predicateOrOptions)) {
+ if (predicateOrOptions.predicate) {
+ predicate = predicateOrOptions.predicate;
+ }
+ if (predicateOrOptions.extensionName) {
+ extensionName = predicateOrOptions.extensionName;
+ }
+ if (typeof predicateOrOptions.priority === "number") {
+ priority = predicateOrOptions.priority;
+ }
+ } else if (predicateOrOptions) {
+ predicate = predicateOrOptions;
+ }
if (!this.extensionPoints[extensionPoint]) {
this.extensionPoints[extensionPoint] = [];
}
const registration = {
- predicate: predicate ? predicate : () => true,
+ predicate,
extension,
- extensionName: extensionName ? extensionName : ""
- };
+ extensionName: extensionName ? extensionName : "",
+ priority,
+ } as ExtensionRegistration;
this.extensionPoints[extensionPoint].push(registration);
}
@@ -128,10 +166,10 @@ export class Binder {
): Array {
let registrations = this.extensionPoints[extensionPoint] || [];
if (props) {
- registrations = registrations.filter(reg => reg.predicate(props));
+ registrations = registrations.filter((reg) => reg.predicate(props));
}
registrations.sort(this.sortExtensions);
- return registrations.map(reg => reg.extension);
+ return registrations.map((reg) => reg.extension);
}
/**
@@ -151,7 +189,11 @@ export class Binder {
const regA = a.extensionName ? a.extensionName.toUpperCase() : "";
const regB = b.extensionName ? b.extensionName.toUpperCase() : "";
- if (regA === "" && regB !== "") {
+ if (a.priority > b.priority) {
+ return -1;
+ } else if (a.priority < b.priority) {
+ return 1;
+ } else if (regA === "" && regB !== "") {
return 1;
} else if (regA !== "" && regB === "") {
return -1;
diff --git a/scm-ui/ui-extensions/src/extensionPoints.ts b/scm-ui/ui-extensions/src/extensionPoints.ts
index 817b4f2563..837f5d139f 100644
--- a/scm-ui/ui-extensions/src/extensionPoints.ts
+++ b/scm-ui/ui-extensions/src/extensionPoints.ts
@@ -135,3 +135,24 @@ export type PrimaryNavigationLogoutButtonExtension = ExtensionPointDefinition<
"primary-navigation.logout",
PrimaryNavigationLogoutButtonProps
>;
+
+export type RepositoryOverviewTopExtensionProps = {
+ page: number;
+ search: string;
+ namespace?: string;
+};
+
+export type RepositoryOverviewTopExtension = ExtensionPointDefinition<
+ "repository.overview.top",
+ React.ComponentType,
+ RepositoryOverviewTopExtensionProps
+>;
+export type RepositoryOverviewLeftExtension = ExtensionPointDefinition<"repository.overview.left", React.ComponentType>;
+export type RepositoryOverviewTitleExtension = ExtensionPointDefinition<
+ "repository.overview.title",
+ React.ComponentType
+>;
+export type RepositoryOverviewSubtitleExtension = ExtensionPointDefinition<
+ "repository.overview.subtitle",
+ React.ComponentType
+>;
diff --git a/scm-ui/ui-styles/src/scm.scss b/scm-ui/ui-styles/src/scm.scss
index 9db74be440..08fa5a8476 100644
--- a/scm-ui/ui-styles/src/scm.scss
+++ b/scm-ui/ui-styles/src/scm.scss
@@ -186,6 +186,10 @@ $light-25: darken($high-contrast-light-gray, 45%);
}
*/
+.has-hover-background-blue:hover {
+ background-color: scale-color($blue, $alpha: -90%);
+}
+
// readability issues with original color
.has-text-warning {
color: #ffb600 !important;
diff --git a/scm-ui/ui-webapp/src/repos/components/list/RepositoryGroupEntry.tsx b/scm-ui/ui-webapp/src/repos/components/list/RepositoryGroupEntry.tsx
index 2b18ca2c88..1c8ff8bfe1 100644
--- a/scm-ui/ui-webapp/src/repos/components/list/RepositoryGroupEntry.tsx
+++ b/scm-ui/ui-webapp/src/repos/components/list/RepositoryGroupEntry.tsx
@@ -23,10 +23,9 @@
*/
import React, { FC } from "react";
import { Link } from "react-router-dom";
-import { Icon, RepositoryEntry } from "@scm-manager/ui-components";
+import { Icon, RepositoryEntry, GroupEntries } from "@scm-manager/ui-components";
import { RepositoryGroup } from "@scm-manager/ui-types";
import { useTranslation } from "react-i18next";
-import GroupEntries from "./GroupEntries";
type Props = {
group: RepositoryGroup;
diff --git a/scm-ui/ui-webapp/src/repos/components/list/RepositoryList.tsx b/scm-ui/ui-webapp/src/repos/components/list/RepositoryList.tsx
index fc9e539ab2..e8b52480e6 100644
--- a/scm-ui/ui-webapp/src/repos/components/list/RepositoryList.tsx
+++ b/scm-ui/ui-webapp/src/repos/components/list/RepositoryList.tsx
@@ -27,20 +27,33 @@ import { NamespaceCollection, Repository } from "@scm-manager/ui-types";
import groupByNamespace from "./groupByNamespace";
import RepositoryGroupEntry from "./RepositoryGroupEntry";
+import { ExtensionPoint } from "@scm-manager/ui-extensions";
type Props = {
repositories: Repository[];
namespaces: NamespaceCollection;
+ page: number;
+ search: string;
+ namespace?: string;
};
class RepositoryList extends React.Component {
render() {
- const { repositories, namespaces } = this.props;
+ const { repositories, namespaces, namespace, page, search } = this.props;
const groups = groupByNamespace(repositories, namespaces);
return (
- {groups.map(group => {
+
+ {groups.map((group) => {
return ;
})}
diff --git a/scm-ui/ui-webapp/src/repos/containers/Overview.tsx b/scm-ui/ui-webapp/src/repos/containers/Overview.tsx
index 9fff58af5b..5a6fc1d3ec 100644
--- a/scm-ui/ui-webapp/src/repos/containers/Overview.tsx
+++ b/scm-ui/ui-webapp/src/repos/containers/Overview.tsx
@@ -26,16 +26,32 @@ import { useHistory, useLocation, useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import {
CreateButton,
+ devices,
LinkPaginator,
Notification,
OverviewPageActions,
Page,
PageActions,
- urls
+ urls,
} from "@scm-manager/ui-components";
import RepositoryList from "../components/list";
import { useNamespaces, useRepositories } from "@scm-manager/ui-api";
import { NamespaceCollection, RepositoryCollection } from "@scm-manager/ui-types";
+import { ExtensionPoint, extensionPoints, useBinder } from "@scm-manager/ui-extensions";
+import styled from "styled-components";
+
+const StickyColumn = styled.div`
+ align-self: flex-start;
+
+ &:empty {
+ display: none;
+ }
+
+ @media (min-width: ${devices.mobile.width}px) {
+ position: sticky;
+ top: 1rem;
+ }
+`;
const useUrlParams = () => {
const params = useParams();
@@ -49,7 +65,7 @@ const useOverviewData = () => {
const search = urls.getQueryStringFromLocation(location);
const request = {
- namespace: namespaces?._embedded.namespaces.find(n => n.namespace === namespace),
+ namespace: namespaces?._embedded.namespaces.find((n) => n.namespace === namespace),
// ui starts counting by 1,
// but backend starts counting by 0
page: page - 1,
@@ -59,7 +75,7 @@ const useOverviewData = () => {
// also do not fetch repositories if an invalid namespace is selected
disabled:
(!!namespace && !namespaces) ||
- (!!namespace && !namespaces?._embedded.namespaces.some(n => n.namespace === namespace))
+ (!!namespace && !namespaces?._embedded.namespaces.some((n) => n.namespace === namespace)),
};
const { isLoading: isLoadingRepositories, error: errorRepositories, data: repositories } = useRepositories(request);
@@ -70,7 +86,7 @@ const useOverviewData = () => {
namespace,
repositories,
search,
- page
+ page,
};
};
@@ -79,15 +95,22 @@ type RepositoriesProps = {
repositories?: RepositoryCollection;
search: string;
page: number;
+ namespace?: string;
};
-const Repositories: FC = ({ namespaces, repositories, search, page }) => {
+const Repositories: FC = ({ namespaces, namespace, repositories, search, page }) => {
const [t] = useTranslation("repos");
if (namespaces && repositories) {
if (repositories._embedded && repositories._embedded.repositories.length > 0) {
return (
<>
-
+
>
);
@@ -103,6 +126,9 @@ const Overview: FC = () => {
const { isLoading, error, namespace, namespaces, repositories, search, page } = useOverviewData();
const history = useHistory();
const [t] = useTranslation("repos");
+ const binder = useBinder();
+
+ const extensions = binder.getExtensions("repository.overview.left");
// we keep the create permission in the state,
// because it does not change during searching or paging
@@ -126,7 +152,7 @@ const Overview: FC = () => {
const allNamespacesPlaceholder = t("overview.allNamespaces");
let namespacesToRender: string[] = [];
if (namespaces) {
- namespacesToRender = [allNamespacesPlaceholder, ...namespaces._embedded.namespaces.map(n => n.namespace).sort()];
+ namespacesToRender = [allNamespacesPlaceholder, ...namespaces._embedded.namespaces.map((n) => n.namespace).sort()];
}
const namespaceSelected = (newNamespace: string) => {
if (newNamespace === allNamespacesPlaceholder) {
@@ -136,16 +162,38 @@ const Overview: FC = () => {
}
};
+ const hasExtensions = extensions.length > 0;
+
return (
-
-
- {showCreateButton ? : null}
+ {t("overview.title")}}
+ subtitle={{t("overview.subtitle")}}
+ loading={isLoading}
+ error={error}
+ >
+
+ {hasExtensions ? (
+
+ {extensions.map((extension) => React.createElement(extension))}
+
+ ) : null}
+
+
+ {showCreateButton ? : null}
+
+
{showActions ? (
n.namespace === namespace) ? namespace : ""
+ namespace && namespaces?._embedded.namespaces.some((n) => n.namespace === namespace) ? namespace : ""
}
groups={namespacesToRender}
groupSelected={namespaceSelected}