mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-14 01:15:44 +01:00
Add extension points for source tree (#1816)
This change will add an extension point which allows to wrap the source tree. This is required in order to use a context provider e.g. to capture a selected file. Another extension point allows to add a row between the row of a file. In order to implement the extension points ui-extensions has now a wrapper property and passes the children of an extension point to implementing extension.
This commit is contained in:
@@ -21,9 +21,10 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import React, { FC } from "react";
|
||||
import ExtensionPoint from "./ExtensionPoint";
|
||||
import { shallow, mount } from "enzyme";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import "@scm-manager/ui-tests/enzyme";
|
||||
import binder from "./binder";
|
||||
|
||||
@@ -89,7 +90,7 @@ describe("ExtensionPoint test", () => {
|
||||
<ExtensionPoint
|
||||
name="something.special"
|
||||
props={{
|
||||
value: "Awesome"
|
||||
value: "Awesome",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -126,7 +127,7 @@ describe("ExtensionPoint test", () => {
|
||||
|
||||
it("should pass the context of the parent component", () => {
|
||||
const UserContext = React.createContext({
|
||||
name: "anonymous"
|
||||
name: "anonymous",
|
||||
});
|
||||
|
||||
type HelloProps = {
|
||||
@@ -148,7 +149,7 @@ describe("ExtensionPoint test", () => {
|
||||
return (
|
||||
<UserContext.Provider
|
||||
value={{
|
||||
name: "Trillian"
|
||||
name: "Trillian",
|
||||
}}
|
||||
>
|
||||
<ExtensionPoint name="hello" />
|
||||
@@ -187,7 +188,7 @@ describe("ExtensionPoint test", () => {
|
||||
};
|
||||
|
||||
mockedBinder.hasExtension.mockReturnValue(true);
|
||||
mockedBinder.getExtension.mockReturnValue(<Label name="One" />);
|
||||
mockedBinder.getExtension.mockReturnValue(Label);
|
||||
|
||||
const rendered = mount(<ExtensionPoint name="something.special" props={{ name: "Two" }} />);
|
||||
expect(rendered.text()).toBe("Extension Two");
|
||||
@@ -203,11 +204,79 @@ describe("ExtensionPoint test", () => {
|
||||
const transformer = (props: object) => {
|
||||
return {
|
||||
...props,
|
||||
name: "Two"
|
||||
name: "Two",
|
||||
};
|
||||
};
|
||||
|
||||
const rendered = mount(<ExtensionPoint name="something.special" propTransformer={transformer} />);
|
||||
expect(rendered.text()).toBe("Extension Two");
|
||||
});
|
||||
|
||||
it("should pass children as props", () => {
|
||||
const label: FC = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
<label>Bound Extension</label>
|
||||
<details>{children}</details>
|
||||
</>
|
||||
);
|
||||
};
|
||||
mockedBinder.hasExtension.mockReturnValue(true);
|
||||
mockedBinder.getExtension.mockReturnValue(label);
|
||||
|
||||
const rendered = mount(
|
||||
<ExtensionPoint name="something.special">
|
||||
<p>Cool stuff</p>
|
||||
</ExtensionPoint>
|
||||
);
|
||||
const text = rendered.text();
|
||||
expect(text).toContain("Bound Extension");
|
||||
expect(text).toContain("Cool stuff");
|
||||
});
|
||||
|
||||
it("should wrap children with multiple extensions", () => {
|
||||
const w1: FC = ({ children }) => (
|
||||
<>
|
||||
<label>Outer {"-> "}</label>
|
||||
<details>{children}</details>
|
||||
</>
|
||||
);
|
||||
|
||||
const w2: FC = ({ children }) => (
|
||||
<>
|
||||
<label>Inner {"-> "}</label>
|
||||
<details>{children}</details>
|
||||
</>
|
||||
);
|
||||
|
||||
mockedBinder.hasExtension.mockReturnValue(true);
|
||||
mockedBinder.getExtensions.mockReturnValue([w1, w2]);
|
||||
|
||||
const rendered = mount(
|
||||
<ExtensionPoint name="something.special" renderAll={true} wrapper={true}>
|
||||
<p>Children</p>
|
||||
</ExtensionPoint>
|
||||
);
|
||||
const text = rendered.text();
|
||||
expect(text).toEqual("Outer -> Inner -> Children");
|
||||
});
|
||||
|
||||
it("should render children of non fc", () => {
|
||||
const nonfc = (
|
||||
<div>
|
||||
<label>Non fc with children</label>
|
||||
</div>
|
||||
);
|
||||
|
||||
mockedBinder.hasExtension.mockReturnValue(true);
|
||||
mockedBinder.getExtension.mockReturnValue(nonfc);
|
||||
|
||||
const rendered = mount(
|
||||
<ExtensionPoint name="something.special">
|
||||
<p>Children</p>
|
||||
</ExtensionPoint>
|
||||
);
|
||||
const text = rendered.text();
|
||||
expect(text).toEqual("Non fc with children");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,9 +21,8 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import * as React from "react";
|
||||
import React, { FC, ReactNode } from "react";
|
||||
import { Binder } from "./binder";
|
||||
import { Component, FC, ReactNode } from "react";
|
||||
import useBinder from "./useBinder";
|
||||
|
||||
type PropTransformer = (props: object) => object;
|
||||
@@ -33,12 +32,14 @@ type Props = {
|
||||
renderAll?: boolean;
|
||||
props?: object;
|
||||
propTransformer?: PropTransformer;
|
||||
wrapper?: boolean;
|
||||
};
|
||||
|
||||
const createInstance = (Component: any, props: object, key?: number) => {
|
||||
const instanceProps = {
|
||||
...props,
|
||||
key
|
||||
...(Component.props || {}),
|
||||
key,
|
||||
};
|
||||
if (React.isValidElement(Component)) {
|
||||
return React.cloneElement(Component, instanceProps);
|
||||
@@ -51,12 +52,28 @@ const renderAllExtensions = (binder: Binder, name: string, props: object) => {
|
||||
return <>{extensions.map((cmp, index) => createInstance(cmp, props, index))}</>;
|
||||
};
|
||||
|
||||
const renderWrapperExtensions = (binder: Binder, name: string, props: object) => {
|
||||
const extensions = [...(binder.getExtensions(name, props) || [])];
|
||||
extensions.reverse();
|
||||
|
||||
let instance: any = null;
|
||||
extensions.forEach((cmp, index) => {
|
||||
let instanceProps = props;
|
||||
if (instance) {
|
||||
instanceProps = { ...props, children: instance };
|
||||
}
|
||||
instance = createInstance(cmp, instanceProps, index);
|
||||
});
|
||||
|
||||
return instance;
|
||||
};
|
||||
|
||||
const renderSingleExtension = (binder: Binder, name: string, props: object) => {
|
||||
const cmp = binder.getExtension(name, props);
|
||||
if (!cmp) {
|
||||
return null;
|
||||
}
|
||||
return createInstance(cmp, props, undefined);
|
||||
return createInstance(cmp, props);
|
||||
};
|
||||
|
||||
const renderDefault = (children: ReactNode) => {
|
||||
@@ -67,11 +84,11 @@ const renderDefault = (children: ReactNode) => {
|
||||
};
|
||||
|
||||
const createRenderProps = (propTransformer?: PropTransformer, props?: object) => {
|
||||
const transform = (props: object) => {
|
||||
const transform = (untransformedProps: object) => {
|
||||
if (!propTransformer) {
|
||||
return props;
|
||||
return untransformedProps;
|
||||
}
|
||||
return propTransformer(props);
|
||||
return propTransformer(untransformedProps);
|
||||
};
|
||||
|
||||
return transform(props || {});
|
||||
@@ -80,12 +97,15 @@ const createRenderProps = (propTransformer?: PropTransformer, props?: object) =>
|
||||
/**
|
||||
* ExtensionPoint renders components which are bound to an extension point.
|
||||
*/
|
||||
const ExtensionPoint: FC<Props> = ({ name, propTransformer, props, renderAll, children }) => {
|
||||
const ExtensionPoint: FC<Props> = ({ name, propTransformer, props, renderAll, wrapper, children }) => {
|
||||
const binder = useBinder();
|
||||
const renderProps = createRenderProps(propTransformer, props);
|
||||
const renderProps = createRenderProps(propTransformer, { ...(props || {}), children });
|
||||
if (!binder.hasExtension(name, renderProps)) {
|
||||
return renderDefault(children);
|
||||
} else if (renderAll) {
|
||||
if (wrapper) {
|
||||
return renderWrapperExtensions(binder, name, renderProps);
|
||||
}
|
||||
return renderAllExtensions(binder, name, renderProps);
|
||||
}
|
||||
return renderSingleExtension(binder, name, renderProps);
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
import React from "react";
|
||||
import {
|
||||
File,
|
||||
Branch,
|
||||
IndexResources,
|
||||
Links,
|
||||
@@ -83,6 +84,31 @@ export type ReposSourcesEmptyActionbar = ExtensionPointDefinition<
|
||||
ReposSourcesEmptyActionbarExtension
|
||||
>;
|
||||
|
||||
export type ReposSourcesTreeWrapperProps = {
|
||||
repository: Repository;
|
||||
directory: File;
|
||||
baseUrl: string;
|
||||
revision: string;
|
||||
};
|
||||
|
||||
export type ReposSourcesTreeWrapperExtension = ExtensionPointDefinition<
|
||||
"repos.source.tree.wrapper",
|
||||
React.ComponentType<ReposSourcesTreeWrapperProps>
|
||||
>;
|
||||
|
||||
export type ReposSourcesTreeRowProps = {
|
||||
file: File;
|
||||
};
|
||||
|
||||
export type ReposSourcesTreeRowRightExtension = ExtensionPointDefinition<
|
||||
"repos.sources.tree.row.right",
|
||||
React.ComponentType<ReposSourcesTreeRowProps>
|
||||
>;
|
||||
export type ReposSourcesTreeRowAfterExtension = ExtensionPointDefinition<
|
||||
"repos.sources.tree.row.after",
|
||||
React.ComponentType<ReposSourcesTreeRowProps>
|
||||
>;
|
||||
|
||||
export type PrimaryNavigationLoginButtonProps = {
|
||||
links: Links;
|
||||
label: string;
|
||||
|
||||
Reference in New Issue
Block a user