mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-13 00:45: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:
@@ -105,7 +105,7 @@ binder.bind("repo.avatar", GitAvatar, (props) => props.type === "git");
|
|||||||
```
|
```
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
<ExtensionPoint name="repo.avatar" props={type: "git"} />
|
<ExtensionPoint name="repo.avatar" props={{type: "git"}} />
|
||||||
```
|
```
|
||||||
|
|
||||||
### Typings
|
### Typings
|
||||||
@@ -141,3 +141,75 @@ Negative Example:
|
|||||||
|
|
||||||
This code for example, would lead to a compile time type error because we made a typo in the `name` of the extension when binding it.
|
This code for example, would lead to a compile time type error because we made a typo in the `name` of the extension when binding it.
|
||||||
If we had used the `bind` method without the type parameter, we would not have gotten an error but run into problems at runtime.
|
If we had used the `bind` method without the type parameter, we would not have gotten an error but run into problems at runtime.
|
||||||
|
|
||||||
|
### Children
|
||||||
|
|
||||||
|
If an extension point defines children those children are propagated to the extensions as children prop e.g:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const MyExtension:FC = ({children}) => (
|
||||||
|
<div className="fancy-box">{children}</div>
|
||||||
|
)
|
||||||
|
const App = () => {
|
||||||
|
binder.bind("box", MyExtension);
|
||||||
|
return (
|
||||||
|
<ExtensionPoint name="box">
|
||||||
|
<p>Box Content</p>
|
||||||
|
</ExtensionPoint>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The example above renders the following html code:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="fancy-box">
|
||||||
|
<p>Box Content</p>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
An exception is when the extension already has a children property, this could be the case if jsx is directly bind.
|
||||||
|
This exception applies not only to the children property it applies to every property.
|
||||||
|
The example below renders `Ahoi`, because the property of the jsx overwrites the one from the extension point.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
type Props = {
|
||||||
|
greeting: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GreetingExtension:FC<Props> = ({greeting}) => (
|
||||||
|
<>{greeting}</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
binder.bind("greet", <GreetingExtension greeting="Ahoi" />);
|
||||||
|
return <ExtensionPoint name="greet" props={{greeting: "Moin"}} />;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wrapper
|
||||||
|
|
||||||
|
Sometimes it can be useful to allow plugin developers to wrap an existing component.
|
||||||
|
The `wrapper` property is exactly for this case, it allows to wrap an existing component with multiple extensions e.g.:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const Outer: FC = ({ children }) => (
|
||||||
|
<>Outer -> {children}</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Inner: FC = ({ children }) => (
|
||||||
|
<>Outer -> {children}</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
binder.bind("wrapped", Outer);
|
||||||
|
binder.bind("wrapped", Inner);
|
||||||
|
return (
|
||||||
|
<ExtensionPoint name="wrapped" renderAll={true} wrapper={true}>
|
||||||
|
Children
|
||||||
|
</ExtensionPoint>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The example above renders `Outer -> Inner -> Children`, because each extension is passed as children to the parent extension.
|
||||||
|
|||||||
2
gradle/changelog/ep_history_download.yaml
Normal file
2
gradle/changelog/ep_history_download.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
- type: Added
|
||||||
|
description: Extension points for source tree ([#1816](https://github.com/scm-manager/scm-manager/pull/1816))
|
||||||
@@ -21,9 +21,10 @@
|
|||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import React from "react";
|
import React, { FC } from "react";
|
||||||
import ExtensionPoint from "./ExtensionPoint";
|
import ExtensionPoint from "./ExtensionPoint";
|
||||||
import { shallow, mount } from "enzyme";
|
import { shallow, mount } from "enzyme";
|
||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import "@scm-manager/ui-tests/enzyme";
|
import "@scm-manager/ui-tests/enzyme";
|
||||||
import binder from "./binder";
|
import binder from "./binder";
|
||||||
|
|
||||||
@@ -89,7 +90,7 @@ describe("ExtensionPoint test", () => {
|
|||||||
<ExtensionPoint
|
<ExtensionPoint
|
||||||
name="something.special"
|
name="something.special"
|
||||||
props={{
|
props={{
|
||||||
value: "Awesome"
|
value: "Awesome",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -126,7 +127,7 @@ describe("ExtensionPoint test", () => {
|
|||||||
|
|
||||||
it("should pass the context of the parent component", () => {
|
it("should pass the context of the parent component", () => {
|
||||||
const UserContext = React.createContext({
|
const UserContext = React.createContext({
|
||||||
name: "anonymous"
|
name: "anonymous",
|
||||||
});
|
});
|
||||||
|
|
||||||
type HelloProps = {
|
type HelloProps = {
|
||||||
@@ -148,7 +149,7 @@ describe("ExtensionPoint test", () => {
|
|||||||
return (
|
return (
|
||||||
<UserContext.Provider
|
<UserContext.Provider
|
||||||
value={{
|
value={{
|
||||||
name: "Trillian"
|
name: "Trillian",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ExtensionPoint name="hello" />
|
<ExtensionPoint name="hello" />
|
||||||
@@ -187,7 +188,7 @@ describe("ExtensionPoint test", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
mockedBinder.hasExtension.mockReturnValue(true);
|
mockedBinder.hasExtension.mockReturnValue(true);
|
||||||
mockedBinder.getExtension.mockReturnValue(<Label name="One" />);
|
mockedBinder.getExtension.mockReturnValue(Label);
|
||||||
|
|
||||||
const rendered = mount(<ExtensionPoint name="something.special" props={{ name: "Two" }} />);
|
const rendered = mount(<ExtensionPoint name="something.special" props={{ name: "Two" }} />);
|
||||||
expect(rendered.text()).toBe("Extension Two");
|
expect(rendered.text()).toBe("Extension Two");
|
||||||
@@ -203,11 +204,79 @@ describe("ExtensionPoint test", () => {
|
|||||||
const transformer = (props: object) => {
|
const transformer = (props: object) => {
|
||||||
return {
|
return {
|
||||||
...props,
|
...props,
|
||||||
name: "Two"
|
name: "Two",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendered = mount(<ExtensionPoint name="something.special" propTransformer={transformer} />);
|
const rendered = mount(<ExtensionPoint name="something.special" propTransformer={transformer} />);
|
||||||
expect(rendered.text()).toBe("Extension Two");
|
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
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import * as React from "react";
|
import React, { FC, ReactNode } from "react";
|
||||||
import { Binder } from "./binder";
|
import { Binder } from "./binder";
|
||||||
import { Component, FC, ReactNode } from "react";
|
|
||||||
import useBinder from "./useBinder";
|
import useBinder from "./useBinder";
|
||||||
|
|
||||||
type PropTransformer = (props: object) => object;
|
type PropTransformer = (props: object) => object;
|
||||||
@@ -33,12 +32,14 @@ type Props = {
|
|||||||
renderAll?: boolean;
|
renderAll?: boolean;
|
||||||
props?: object;
|
props?: object;
|
||||||
propTransformer?: PropTransformer;
|
propTransformer?: PropTransformer;
|
||||||
|
wrapper?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createInstance = (Component: any, props: object, key?: number) => {
|
const createInstance = (Component: any, props: object, key?: number) => {
|
||||||
const instanceProps = {
|
const instanceProps = {
|
||||||
...props,
|
...props,
|
||||||
key
|
...(Component.props || {}),
|
||||||
|
key,
|
||||||
};
|
};
|
||||||
if (React.isValidElement(Component)) {
|
if (React.isValidElement(Component)) {
|
||||||
return React.cloneElement(Component, instanceProps);
|
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))}</>;
|
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 renderSingleExtension = (binder: Binder, name: string, props: object) => {
|
||||||
const cmp = binder.getExtension(name, props);
|
const cmp = binder.getExtension(name, props);
|
||||||
if (!cmp) {
|
if (!cmp) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return createInstance(cmp, props, undefined);
|
return createInstance(cmp, props);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderDefault = (children: ReactNode) => {
|
const renderDefault = (children: ReactNode) => {
|
||||||
@@ -67,11 +84,11 @@ const renderDefault = (children: ReactNode) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createRenderProps = (propTransformer?: PropTransformer, props?: object) => {
|
const createRenderProps = (propTransformer?: PropTransformer, props?: object) => {
|
||||||
const transform = (props: object) => {
|
const transform = (untransformedProps: object) => {
|
||||||
if (!propTransformer) {
|
if (!propTransformer) {
|
||||||
return props;
|
return untransformedProps;
|
||||||
}
|
}
|
||||||
return propTransformer(props);
|
return propTransformer(untransformedProps);
|
||||||
};
|
};
|
||||||
|
|
||||||
return transform(props || {});
|
return transform(props || {});
|
||||||
@@ -80,12 +97,15 @@ const createRenderProps = (propTransformer?: PropTransformer, props?: object) =>
|
|||||||
/**
|
/**
|
||||||
* ExtensionPoint renders components which are bound to an extension point.
|
* 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 binder = useBinder();
|
||||||
const renderProps = createRenderProps(propTransformer, props);
|
const renderProps = createRenderProps(propTransformer, { ...(props || {}), children });
|
||||||
if (!binder.hasExtension(name, renderProps)) {
|
if (!binder.hasExtension(name, renderProps)) {
|
||||||
return renderDefault(children);
|
return renderDefault(children);
|
||||||
} else if (renderAll) {
|
} else if (renderAll) {
|
||||||
|
if (wrapper) {
|
||||||
|
return renderWrapperExtensions(binder, name, renderProps);
|
||||||
|
}
|
||||||
return renderAllExtensions(binder, name, renderProps);
|
return renderAllExtensions(binder, name, renderProps);
|
||||||
}
|
}
|
||||||
return renderSingleExtension(binder, name, renderProps);
|
return renderSingleExtension(binder, name, renderProps);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
|
File,
|
||||||
Branch,
|
Branch,
|
||||||
IndexResources,
|
IndexResources,
|
||||||
Links,
|
Links,
|
||||||
@@ -83,6 +84,31 @@ export type ReposSourcesEmptyActionbar = ExtensionPointDefinition<
|
|||||||
ReposSourcesEmptyActionbarExtension
|
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 = {
|
export type PrimaryNavigationLoginButtonProps = {
|
||||||
links: Links;
|
links: Links;
|
||||||
label: string;
|
label: string;
|
||||||
|
|||||||
@@ -25,14 +25,15 @@
|
|||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { binder } from "@scm-manager/ui-extensions";
|
import { binder, ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||||
import { File } from "@scm-manager/ui-types";
|
import { File, Repository } from "@scm-manager/ui-types";
|
||||||
import { Notification } from "@scm-manager/ui-components";
|
|
||||||
import FileTreeLeaf from "./FileTreeLeaf";
|
import FileTreeLeaf from "./FileTreeLeaf";
|
||||||
import TruncatedNotification from "./TruncatedNotification";
|
import TruncatedNotification from "./TruncatedNotification";
|
||||||
import {isRootPath} from "../utils/files";
|
import { isRootPath } from "../utils/files";
|
||||||
|
import { extensionPoints } from "@scm-manager/ui-extensions";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
repository: Repository;
|
||||||
directory: File;
|
directory: File;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
revision: string;
|
revision: string;
|
||||||
@@ -56,7 +57,7 @@ export function findParent(path: string) {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const FileTree: FC<Props> = ({ directory, baseUrl, revision, fetchNextPage, isFetchingNextPage }) => {
|
const FileTree: FC<Props> = ({ repository, directory, baseUrl, revision, fetchNextPage, isFetchingNextPage }) => {
|
||||||
const [t] = useTranslation("repos");
|
const [t] = useTranslation("repos");
|
||||||
const { path } = directory;
|
const { path } = directory;
|
||||||
const files: File[] = [];
|
const files: File[] = [];
|
||||||
@@ -69,8 +70,8 @@ const FileTree: FC<Props> = ({ directory, baseUrl, revision, fetchNextPage, isFe
|
|||||||
revision,
|
revision,
|
||||||
_links: {},
|
_links: {},
|
||||||
_embedded: {
|
_embedded: {
|
||||||
children: []
|
children: [],
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,30 +79,39 @@ const FileTree: FC<Props> = ({ directory, baseUrl, revision, fetchNextPage, isFe
|
|||||||
|
|
||||||
const baseUrlWithRevision = baseUrl + "/" + encodeURIComponent(revision);
|
const baseUrlWithRevision = baseUrl + "/" + encodeURIComponent(revision);
|
||||||
|
|
||||||
|
const extProps: extensionPoints.ReposSourcesTreeWrapperProps = {
|
||||||
|
repository,
|
||||||
|
directory,
|
||||||
|
baseUrl,
|
||||||
|
revision,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="panel-block">
|
<div className="panel-block">
|
||||||
<table className="table table-hover table-sm is-fullwidth">
|
<ExtensionPoint name="repos.source.tree.wrapper" props={extProps} renderAll={true} wrapper={true}>
|
||||||
<thead>
|
<table className="table table-hover table-sm is-fullwidth">
|
||||||
<tr>
|
<thead>
|
||||||
<FixedWidthTh />
|
<tr>
|
||||||
<th>{t("sources.fileTree.name")}</th>
|
<FixedWidthTh />
|
||||||
<th className="is-hidden-mobile">{t("sources.fileTree.length")}</th>
|
<th>{t("sources.fileTree.name")}</th>
|
||||||
<th className="is-hidden-mobile">{t("sources.fileTree.commitDate")}</th>
|
<th className="is-hidden-mobile">{t("sources.fileTree.length")}</th>
|
||||||
<th className="is-hidden-touch">{t("sources.fileTree.description")}</th>
|
<th className="is-hidden-mobile">{t("sources.fileTree.commitDate")}</th>
|
||||||
{binder.hasExtension("repos.sources.tree.row.right") && <th className="is-hidden-mobile" />}
|
<th className="is-hidden-touch">{t("sources.fileTree.description")}</th>
|
||||||
</tr>
|
{binder.hasExtension("repos.sources.tree.row.right") && <th className="is-hidden-mobile" />}
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
</thead>
|
||||||
{files.map((file: File) => (
|
<tbody>
|
||||||
<FileTreeLeaf key={file.name} file={file} baseUrl={baseUrlWithRevision} />
|
{files.map((file: File) => (
|
||||||
))}
|
<FileTreeLeaf key={file.name} file={file} baseUrl={baseUrlWithRevision} />
|
||||||
</tbody>
|
))}
|
||||||
</table>
|
</tbody>
|
||||||
<TruncatedNotification
|
</table>
|
||||||
directory={directory}
|
<TruncatedNotification
|
||||||
fetchNextPage={fetchNextPage}
|
directory={directory}
|
||||||
isFetchingNextPage={isFetchingNextPage}
|
fetchNextPage={fetchNextPage}
|
||||||
/>
|
isFetchingNextPage={isFetchingNextPage}
|
||||||
|
/>
|
||||||
|
</ExtensionPoint>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import * as React from "react";
|
|||||||
import { WithTranslation, withTranslation } from "react-i18next";
|
import { WithTranslation, withTranslation } from "react-i18next";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { binder, ExtensionPoint } from "@scm-manager/ui-extensions";
|
import { binder, ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions";
|
||||||
import { File } from "@scm-manager/ui-types";
|
import { File } from "@scm-manager/ui-types";
|
||||||
import { DateFromNow, FileSize, Tooltip, Icon } from "@scm-manager/ui-components";
|
import { DateFromNow, FileSize, Tooltip, Icon } from "@scm-manager/ui-components";
|
||||||
import FileIcon from "./FileIcon";
|
import FileIcon from "./FileIcon";
|
||||||
@@ -44,6 +44,14 @@ const NoWrapTd = styled.td`
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const ExtensionTd = styled.td`
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
> *:not(:last-child) {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
class FileTreeLeaf extends React.Component<Props> {
|
class FileTreeLeaf extends React.Component<Props> {
|
||||||
createFileIcon = (file: File) => {
|
createFileIcon = (file: File) => {
|
||||||
return (
|
return (
|
||||||
@@ -88,31 +96,32 @@ class FileTreeLeaf extends React.Component<Props> {
|
|||||||
const renderFileSize = (file: File) => <FileSize bytes={file?.length ? file.length : 0} />;
|
const renderFileSize = (file: File) => <FileSize bytes={file?.length ? file.length : 0} />;
|
||||||
const renderCommitDate = (file: File) => <DateFromNow date={file.commitDate} />;
|
const renderCommitDate = (file: File) => <DateFromNow date={file.commitDate} />;
|
||||||
|
|
||||||
|
const extProps: extensionPoints.ReposSourcesTreeRowProps = {
|
||||||
|
file,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr>
|
<>
|
||||||
<td>{this.createFileIcon(file)}</td>
|
<tr>
|
||||||
<MinWidthTd className="is-word-break">{this.createFileName(file)}</MinWidthTd>
|
<td>{this.createFileIcon(file)}</td>
|
||||||
<NoWrapTd className="is-hidden-mobile">
|
<MinWidthTd className="is-word-break">{this.createFileName(file)}</MinWidthTd>
|
||||||
{file.directory ? "" : this.contentIfPresent(file, "length", renderFileSize)}
|
<NoWrapTd className="is-hidden-mobile">
|
||||||
</NoWrapTd>
|
{file.directory ? "" : this.contentIfPresent(file, "length", renderFileSize)}
|
||||||
<td className="is-hidden-mobile">{this.contentIfPresent(file, "commitDate", renderCommitDate)}</td>
|
</NoWrapTd>
|
||||||
<MinWidthTd className={classNames("is-word-break", "is-hidden-touch")}>
|
<td className="is-hidden-mobile">{this.contentIfPresent(file, "commitDate", renderCommitDate)}</td>
|
||||||
{this.contentIfPresent(file, "description", file => file.description)}
|
<MinWidthTd className={classNames("is-word-break", "is-hidden-touch")}>
|
||||||
</MinWidthTd>
|
{this.contentIfPresent(file, "description", (file) => file.description)}
|
||||||
{binder.hasExtension("repos.sources.tree.row.right") && (
|
</MinWidthTd>
|
||||||
<td className="is-hidden-mobile">
|
{binder.hasExtension("repos.sources.tree.row.right") && (
|
||||||
{!file.directory && (
|
<ExtensionTd className="is-hidden-mobile">
|
||||||
<ExtensionPoint
|
{!file.directory && (
|
||||||
name="repos.sources.tree.row.right"
|
<ExtensionPoint name="repos.sources.tree.row.right" props={extProps} renderAll={true} />
|
||||||
props={{
|
)}
|
||||||
file
|
</ExtensionTd>
|
||||||
}}
|
)}
|
||||||
renderAll={true}
|
</tr>
|
||||||
/>
|
<ExtensionPoint name="repos.sources.tree.row.after" props={extProps} renderAll={true} />
|
||||||
)}
|
</>
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
</tr>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ const Sources: FC<Props> = ({ repository, branches, selectedBranch, baseUrl }) =
|
|||||||
} else {
|
} else {
|
||||||
body = (
|
body = (
|
||||||
<FileTree
|
<FileTree
|
||||||
|
repository={repository}
|
||||||
directory={file}
|
directory={file}
|
||||||
revision={revision || file.revision}
|
revision={revision || file.revision}
|
||||||
baseUrl={baseUrl + "/sources"}
|
baseUrl={baseUrl + "/sources"}
|
||||||
|
|||||||
Reference in New Issue
Block a user