Add ConfigurationAdapterBase and extension points for trash bin

Adds the new abstract class ConfigurationAdapterBase to simplify the creation of global configuration views. In addition there is some cleanup, interfaces and extension points for the repository trash bin plugin.

Committed-by: Eduard Heimbuch <eduard.heimbuch@cloudogu.com>
Co-authored-by: Eduard Heimbuch <eduard.heimbuch@cloudogu.com>
Co-authored-by: René Pfeuffer <rene.pfeuffer@cloudogu.com>
Co-authored-by: Konstantin Schaper <konstantin.schaper@cloudogu.com>
This commit is contained in:
Eduard Heimbuch
2023-01-12 14:01:04 +01:00
parent 5c4c759bd2
commit ac419daa3f
34 changed files with 879 additions and 84 deletions

View File

@@ -21,16 +21,16 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC } from "react";
import React, { ComponentProps, FC } from "react";
import { BackendError } from "@scm-manager/ui-api";
import Notification from "./Notification";
import { useTranslation } from "react-i18next";
type Props = {
type Props = Omit<ComponentProps<typeof Notification>, "type" | "role"> & {
error: BackendError;
};
const BackendErrorNotification: FC<Props> = ({ error }) => {
const BackendErrorNotification: FC<Props> = ({ error, ...props }) => {
const [t] = useTranslation("plugins");
const renderErrorName = () => {
@@ -141,7 +141,7 @@ const BackendErrorNotification: FC<Props> = ({ error }) => {
};
return (
<Notification type="danger" role="alert">
<Notification type="danger" role="alert" {...props}>
<div className="content">
<p className="subtitle">
{t("error.subtitle")}

View File

@@ -21,14 +21,14 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC } from "react";
import React, { ComponentProps, FC } from "react";
import { useTranslation } from "react-i18next";
import { BackendError, ForbiddenError, UnauthorizedError, urls } from "@scm-manager/ui-api";
import Notification from "./Notification";
import BackendErrorNotification from "./BackendErrorNotification";
import { useLocation } from "react-router-dom";
type Props = {
type Props = ComponentProps<typeof BasicErrorMessage> & {
error?: Error | null;
};
@@ -40,31 +40,31 @@ const LoginLink: FC = () => {
return <a href={urls.withContextPath(`/login?from=${from}`)}>{t("errorNotification.loginLink")}</a>;
};
const BasicErrorMessage: FC = ({ children }) => {
const BasicErrorMessage: FC<Omit<ComponentProps<typeof Notification>, "type" | "role">> = ({ children, ...props }) => {
const [t] = useTranslation("commons");
return (
<Notification type="danger" role="alert">
<Notification type="danger" role="alert" {...props}>
<strong>{t("errorNotification.prefix")}:</strong> {children}
</Notification>
);
};
const ErrorNotification: FC<Props> = ({ error }) => {
const ErrorNotification: FC<Props> = ({ error, ...props }) => {
const [t] = useTranslation("commons");
if (error) {
if (error instanceof BackendError) {
return <BackendErrorNotification error={error} />;
return <BackendErrorNotification error={error} {...props} />;
} else if (error instanceof UnauthorizedError) {
return (
<BasicErrorMessage>
<BasicErrorMessage {...props}>
{t("errorNotification.timeout")} <LoginLink />
</BasicErrorMessage>
);
} else if (error instanceof ForbiddenError) {
return <BasicErrorMessage>{t("errorNotification.forbidden")}</BasicErrorMessage>;
return <BasicErrorMessage {...props}>{t("errorNotification.forbidden")}</BasicErrorMessage>;
} else {
return <BasicErrorMessage>{error.message}</BasicErrorMessage>;
return <BasicErrorMessage {...props}>{error.message}</BasicErrorMessage>;
}
}
return null;

View File

@@ -21,12 +21,12 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from "react";
import { binder } from "@scm-manager/ui-extensions";
import React, { ComponentProps } from "react";
import { binder, extensionPoints } from "@scm-manager/ui-extensions";
import { NavLink } from "../navigation";
import { Route } from "react-router-dom";
import { WithTranslation, withTranslation } from "react-i18next";
import { Repository, Links, Link } from "@scm-manager/ui-types";
import { Link, Links, Repository } from "@scm-manager/ui-types";
import { urls } from "@scm-manager/ui-api";
type GlobalRouteProps = {
@@ -44,8 +44,13 @@ type RepositoryNavProps = WithTranslation & { url: string };
class ConfigurationBinder {
i18nNamespace = "plugins";
navLink(to: string, labelI18nKey: string, t: any) {
return <NavLink to={to} label={t(labelI18nKey)} />;
navLink(
to: string,
labelI18nKey: string,
t: any,
options: Omit<ComponentProps<typeof NavLink>, "label" | "to"> = {}
) {
return <NavLink to={to} label={t(labelI18nKey)} {...options} />;
}
route(path: string, Component: any) {
@@ -86,6 +91,27 @@ class ConfigurationBinder {
binder.bind("admin.route", ConfigRoute, configPredicate);
}
bindAdmin(
to: string,
labelI18nKey: string,
icon: string,
linkName: string,
Component: React.ComponentType<{ link: string }>
) {
const predicate = ({ links }: extensionPoints.AdminRoute["props"]) => links[linkName];
const AdminNavLink = withTranslation(this.i18nNamespace)(
({ t, url }: WithTranslation & extensionPoints.AdminNavigation["props"]) =>
this.navLink(url + to, labelI18nKey, t, { icon })
);
const AdminRoute: extensionPoints.AdminRoute["type"] = ({ links, url }) =>
this.route(url + to, <Component link={(links[linkName] as Link).href} />);
binder.bind<extensionPoints.AdminRoute>("admin.route", AdminRoute, predicate);
binder.bind<extensionPoints.AdminNavigation>("admin.navigation", AdminNavLink, predicate);
}
bindRepository(to: string, labelI18nKey: string, linkName: string, RepositoryComponent: any) {
// create predicate based on the link name of the current repository route
// if the linkname is not available, the navigation link and the route are not bound to the extension points