mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-09 06:55:47 +01:00
Add keyboard navigation for users, groups, branches, tags, sources, changesets and plugins (#2153)
Co-authored-by: Eduard Heimbuch <eduard.heimbuch@cloudogu.com>
This commit is contained in:
committed by
GitHub
parent
da71004dd0
commit
eea60deadb
2
gradle/changelog/list_navigation.yaml
Normal file
2
gradle/changelog/list_navigation.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
- type: added
|
||||||
|
description: Keyboard navigation for users, groups, branches, tags, sources, changesets and plugins ([#2153](https://github.com/scm-manager/scm-manager/pull/2153))
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@scm-manager/ui-syntaxhighlighting": "2.39.2-SNAPSHOT",
|
"@scm-manager/ui-syntaxhighlighting": "2.39.2-SNAPSHOT",
|
||||||
|
"@scm-manager/ui-shortcuts": "2.39.2-SNAPSHOT",
|
||||||
"@scm-manager/ui-text": "2.39.2-SNAPSHOT",
|
"@scm-manager/ui-text": "2.39.2-SNAPSHOT",
|
||||||
"@scm-manager/babel-preset": "^2.13.1",
|
"@scm-manager/babel-preset": "^2.13.1",
|
||||||
"@scm-manager/eslint-config": "^2.16.0",
|
"@scm-manager/eslint-config": "^2.16.0",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
* 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, { FC, ReactNode } from "react";
|
import React, { ReactNode, useCallback } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
@@ -55,69 +55,72 @@ const InvisibleButton = styled.button`
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const CardColumn: FC<Props> = ({
|
const CardColumn = React.forwardRef<HTMLElement, Props>(
|
||||||
link,
|
({ link, avatar, title, description, contentRight, footerLeft, footerRight, action, className }, ref) => {
|
||||||
avatar,
|
const renderAvatar = avatar ? <figure className="media-left mt-3 ml-4">{avatar}</figure> : null;
|
||||||
title,
|
const renderDescription = description ? <p className="shorten-text">{description}</p> : null;
|
||||||
description,
|
const renderContentRight = contentRight ? <div className="ml-auto">{contentRight}</div> : null;
|
||||||
contentRight,
|
const executeRef = useCallback(
|
||||||
footerLeft,
|
(el: HTMLButtonElement | HTMLAnchorElement | null) => {
|
||||||
footerRight,
|
if (typeof ref === "function") {
|
||||||
action,
|
ref(el);
|
||||||
className,
|
} else if (ref) {
|
||||||
}) => {
|
ref.current = el;
|
||||||
const renderAvatar = avatar ? <figure className="media-left mt-3 ml-4">{avatar}</figure> : null;
|
}
|
||||||
const renderDescription = description ? <p className="shorten-text">{description}</p> : null;
|
},
|
||||||
const renderContentRight = contentRight ? <div className="ml-auto">{contentRight}</div> : null;
|
[ref]
|
||||||
|
);
|
||||||
|
|
||||||
let createLink = null;
|
let createLink = null;
|
||||||
if (link) {
|
if (link) {
|
||||||
createLink = <Link className="overlay-column" to={link} />;
|
createLink = <Link ref={executeRef} className="overlay-column" to={link} />;
|
||||||
} else if (action) {
|
} else if (action) {
|
||||||
createLink = (
|
createLink = (
|
||||||
<InvisibleButton
|
<InvisibleButton
|
||||||
className="overlay-column"
|
ref={executeRef}
|
||||||
onClick={(e) => {
|
className="overlay-column"
|
||||||
e.preventDefault();
|
onClick={(e) => {
|
||||||
action();
|
e.preventDefault();
|
||||||
}}
|
action();
|
||||||
tabIndex={0}
|
}}
|
||||||
/>
|
tabIndex={0}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{createLink}
|
||||||
|
<NoEventWrapper className={classNames("media", className)}>
|
||||||
|
{renderAvatar}
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"media-content",
|
||||||
|
"text-box",
|
||||||
|
"is-flex",
|
||||||
|
"is-flex-direction-column",
|
||||||
|
"is-justify-content-space-around",
|
||||||
|
"is-align-self-stretch"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="is-flex">
|
||||||
|
<div className="is-clipped mb-0">
|
||||||
|
<p className="shorten-text m-0">{title}</p>
|
||||||
|
{renderDescription}
|
||||||
|
</div>
|
||||||
|
{renderContentRight}
|
||||||
|
</div>
|
||||||
|
<div className={classNames("level", "is-flex", "pb-4")}>
|
||||||
|
<div className={classNames("level-left", "is-hidden-mobile", "mr-2")}>{footerLeft}</div>
|
||||||
|
<InheritFlexShrinkDiv className="level-right is-block is-mobile m-0 shorten-text">
|
||||||
|
{footerRight}
|
||||||
|
</InheritFlexShrinkDiv>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NoEventWrapper>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{createLink}
|
|
||||||
<NoEventWrapper className={classNames("media", className)}>
|
|
||||||
{renderAvatar}
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
"media-content",
|
|
||||||
"text-box",
|
|
||||||
"is-flex",
|
|
||||||
"is-flex-direction-column",
|
|
||||||
"is-justify-content-space-around",
|
|
||||||
"is-align-self-stretch"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="is-flex">
|
|
||||||
<div className="is-clipped mb-0">
|
|
||||||
<p className="shorten-text m-0">{title}</p>
|
|
||||||
{renderDescription}
|
|
||||||
</div>
|
|
||||||
{renderContentRight}
|
|
||||||
</div>
|
|
||||||
<div className={classNames("level", "is-flex", "pb-4")}>
|
|
||||||
<div className={classNames("level-left", "is-hidden-mobile", "mr-2")}>{footerLeft}</div>
|
|
||||||
<InheritFlexShrinkDiv className="level-right is-block is-mobile m-0 shorten-text">
|
|
||||||
{footerRight}
|
|
||||||
</InheritFlexShrinkDiv>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</NoEventWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CardColumn;
|
export default CardColumn;
|
||||||
|
|||||||
@@ -41,13 +41,13 @@ class CardColumnGroup extends React.Component<Props, State> {
|
|||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
collapsed: false
|
collapsed: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleCollapse = () => {
|
toggleCollapse = () => {
|
||||||
this.setState(prevState => ({
|
this.setState((prevState) => ({
|
||||||
collapsed: !prevState.collapsed
|
collapsed: !prevState.collapsed,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -75,7 +75,10 @@ class CardColumnGroup extends React.Component<Props, State> {
|
|||||||
const fullColumnWidth = this.isFullSize(elements, index);
|
const fullColumnWidth = this.isFullSize(elements, index);
|
||||||
const sizeClass = fullColumnWidth ? "is-full" : "is-half";
|
const sizeClass = fullColumnWidth ? "is-full" : "is-half";
|
||||||
return (
|
return (
|
||||||
<div className={classNames("box", "box-link-shadow", "column", "is-clipped", sizeClass)} key={index}>
|
<div
|
||||||
|
className={classNames("box", "box-link-shadow", "column", "is-relative", "is-clipped", sizeClass)}
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
{entry}
|
{entry}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -934,6 +934,24 @@ exports[`Storyshots Buttons/Button Loading 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`Storyshots Buttons/Button Ref Default 1`] = `
|
||||||
|
<div
|
||||||
|
className="indexstories__Spacing-sc-bpoict-0 jXUBTh"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="button is-default"
|
||||||
|
onClick={[Function]}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
|
||||||
|
|
||||||
|
Focus me!
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`Storyshots Buttons/CreateButton Default 1`] = `
|
exports[`Storyshots Buttons/CreateButton Default 1`] = `
|
||||||
<div
|
<div
|
||||||
className="indexstories__Spacing-sc-bpoict-0 jXUBTh"
|
className="indexstories__Spacing-sc-bpoict-0 jXUBTh"
|
||||||
@@ -20096,7 +20114,7 @@ exports[`Storyshots Repositories/Changesets Co-Authors with avatar 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.details"
|
aria-label="changeset.buttons.details"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/changeset/b6c6f8fbd0d490936fae7d26ffdd4695cc2a0930"
|
href="/repo/hitchhiker/heartOfGold/code/changeset/b6c6f8fbd0d490936fae7d26ffdd4695cc2a0930"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -20116,7 +20134,7 @@ exports[`Storyshots Repositories/Changesets Co-Authors with avatar 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.sources"
|
aria-label="changeset.buttons.sources"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/sources/b6c6f8fbd0d490936fae7d26ffdd4695cc2a0930"
|
href="/repo/hitchhiker/heartOfGold/code/sources/b6c6f8fbd0d490936fae7d26ffdd4695cc2a0930"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -20253,7 +20271,7 @@ exports[`Storyshots Repositories/Changesets Commiter and Co-Authors with avatar
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.details"
|
aria-label="changeset.buttons.details"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/changeset/a88567ef1e9528a700555cad8c4576b72fc7c6dd"
|
href="/repo/hitchhiker/heartOfGold/code/changeset/a88567ef1e9528a700555cad8c4576b72fc7c6dd"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -20273,7 +20291,7 @@ exports[`Storyshots Repositories/Changesets Commiter and Co-Authors with avatar
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.sources"
|
aria-label="changeset.buttons.sources"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/sources/a88567ef1e9528a700555cad8c4576b72fc7c6dd"
|
href="/repo/hitchhiker/heartOfGold/code/sources/a88567ef1e9528a700555cad8c4576b72fc7c6dd"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -20368,7 +20386,7 @@ exports[`Storyshots Repositories/Changesets Default 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.details"
|
aria-label="changeset.buttons.details"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -20388,7 +20406,371 @@ exports[`Storyshots Repositories/Changesets Default 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.sources"
|
aria-label="changeset.buttons.sources"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
|
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<i
|
||||||
|
className="fas fa-fw fa-code has-text-inherit is-medium pr-5"
|
||||||
|
onKeyPress={[Function]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
changeset.buttons.sources
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Storyshots Repositories/Changesets List with navigation 1`] = `
|
||||||
|
<div
|
||||||
|
className="Changesetsstories__Wrapper-sc-122npan-0 bEvXuM box box-link-shadow"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="ChangesetRow__Wrapper-sc-tkpti5-0 fyJvMU"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="columns is-variable is-1-mobile is-0-tablet"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="column is-three-fifths is-full-mobile"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="columns is-gapless"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="column is-four-fifths"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="media"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="SingleChangeset__FullWidthDiv-sc-ytpqp9-1 gydPEK media-right ml-0"
|
||||||
|
>
|
||||||
|
<h4
|
||||||
|
className="has-text-weight-bold is-ellipsis-overflow"
|
||||||
|
>
|
||||||
|
|
||||||
|
The starship Heart of Gold was the first spacecraft to make use of the Infinite Improbability Drive. The craft was stolen by then-President Zaphod Beeblebrox at the official launch of the ship, as he was supposed to be officiating the launch. Later, during the use of the Infinite Improbability Drive, the ship picked up Arthur Dent and Ford Prefect, who were floating unprotected in deep space in the same star sector, having just escaped the destruction of the same planet.
|
||||||
|
|
||||||
|
</h4>
|
||||||
|
<p
|
||||||
|
className="is-hidden-touch"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
className="is-hidden-desktop"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="is-flex"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
className="is-size-7 is-ellipsis-overflow mt-2"
|
||||||
|
>
|
||||||
|
changeset.contributors.authoredBy
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="mailto:scm-admin@scm-manager.org"
|
||||||
|
title="changeset.contributors.mailto scm-admin@scm-manager.org"
|
||||||
|
>
|
||||||
|
SCM Administrator
|
||||||
|
</a>
|
||||||
|
,
|
||||||
|
changeset.contributors.committedBy
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="mailto:zaphod.beeblebrox@hitchhiker.cm"
|
||||||
|
title="changeset.contributors.mailto zaphod.beeblebrox@hitchhiker.cm"
|
||||||
|
>
|
||||||
|
Zaphod Beeblebrox
|
||||||
|
</a>
|
||||||
|
|
||||||
|
commaSeparatedList.lastDivider
|
||||||
|
|
||||||
|
changeset.contributors.coAuthoredBy
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="mailto:ford.prefect@hitchhiker.com"
|
||||||
|
title="changeset.contributors.mailto ford.prefect@hitchhiker.com"
|
||||||
|
>
|
||||||
|
Ford Prefect
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="column is-align-self-center"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="column is-flex is-justify-content-flex-end is-align-items-center"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="ButtonAddons__Flex-sc-182golj-0 dcwwZy field has-addons m-0"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
className="control"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
aria-label="changeset.buttons.details"
|
||||||
|
className="button is-default is-reduced-mobile px-3"
|
||||||
|
href="/repo/hitchhiker/heartOfGold/code/changeset/a88567ef1e9528a700555cad8c4576b72fc7c6dd"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<i
|
||||||
|
className="fas fa-fw fa-exchange-alt has-text-inherit is-medium pr-5"
|
||||||
|
onKeyPress={[Function]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
changeset.buttons.details
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
className="control"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
aria-label="changeset.buttons.sources"
|
||||||
|
className="button is-default is-reduced-mobile px-3"
|
||||||
|
href="/repo/hitchhiker/heartOfGold/code/sources/a88567ef1e9528a700555cad8c4576b72fc7c6dd"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<i
|
||||||
|
className="fas fa-fw fa-code has-text-inherit is-medium pr-5"
|
||||||
|
onKeyPress={[Function]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
changeset.buttons.sources
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="ChangesetRow__Wrapper-sc-tkpti5-0 fyJvMU"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="columns is-variable is-1-mobile is-0-tablet"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="column is-three-fifths is-full-mobile"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="columns is-gapless"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="column is-four-fifths"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="media"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="SingleChangeset__FullWidthDiv-sc-ytpqp9-1 gydPEK media-right ml-0"
|
||||||
|
>
|
||||||
|
<h4
|
||||||
|
className="has-text-weight-bold is-ellipsis-overflow"
|
||||||
|
>
|
||||||
|
|
||||||
|
Change heading to "Heart Of Gold"
|
||||||
|
|
||||||
|
</h4>
|
||||||
|
<p
|
||||||
|
className="is-hidden-touch"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
className="is-hidden-desktop"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="is-flex"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
className="is-size-7 is-ellipsis-overflow mt-2"
|
||||||
|
>
|
||||||
|
changeset.contributors.authoredBy
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="mailto:scm-admin@scm-manager.org"
|
||||||
|
title="changeset.contributors.mailto scm-admin@scm-manager.org"
|
||||||
|
>
|
||||||
|
SCM Administrator
|
||||||
|
</a>
|
||||||
|
|
||||||
|
commaSeparatedList.lastDivider
|
||||||
|
|
||||||
|
changeset.contributors.committedBy
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="mailto:zaphod.beeblebrox@hitchhiker.cm"
|
||||||
|
title="changeset.contributors.mailto zaphod.beeblebrox@hitchhiker.cm"
|
||||||
|
>
|
||||||
|
Zaphod Beeblebrox
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="column is-align-self-center"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="column is-flex is-justify-content-flex-end is-align-items-center"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="ButtonAddons__Flex-sc-182golj-0 dcwwZy field has-addons m-0"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
className="control"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
aria-label="changeset.buttons.details"
|
||||||
|
className="button is-default is-reduced-mobile px-3"
|
||||||
|
href="/repo/hitchhiker/heartOfGold/code/changeset/d21cc6c359270aef2196796f4d96af65f51866dc"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<i
|
||||||
|
className="fas fa-fw fa-exchange-alt has-text-inherit is-medium pr-5"
|
||||||
|
onKeyPress={[Function]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
changeset.buttons.details
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
className="control"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
aria-label="changeset.buttons.sources"
|
||||||
|
className="button is-default is-reduced-mobile px-3"
|
||||||
|
href="/repo/hitchhiker/heartOfGold/code/sources/d21cc6c359270aef2196796f4d96af65f51866dc"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<i
|
||||||
|
className="fas fa-fw fa-code has-text-inherit is-medium pr-5"
|
||||||
|
onKeyPress={[Function]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
changeset.buttons.sources
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="ChangesetRow__Wrapper-sc-tkpti5-0 fyJvMU"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="columns is-variable is-1-mobile is-0-tablet"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="column is-three-fifths is-full-mobile"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="columns is-gapless"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="column is-four-fifths"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="media"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="SingleChangeset__FullWidthDiv-sc-ytpqp9-1 gydPEK media-right ml-0"
|
||||||
|
>
|
||||||
|
<h4
|
||||||
|
className="has-text-weight-bold is-ellipsis-overflow"
|
||||||
|
>
|
||||||
|
|
||||||
|
initialize repository
|
||||||
|
|
||||||
|
</h4>
|
||||||
|
<p
|
||||||
|
className="is-hidden-touch"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
className="is-hidden-desktop"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="is-flex"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
className="is-size-7 is-ellipsis-overflow mt-2"
|
||||||
|
>
|
||||||
|
changeset.contributors.authoredBy
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="mailto:scm-admin@scm-manager.org"
|
||||||
|
title="changeset.contributors.mailto scm-admin@scm-manager.org"
|
||||||
|
>
|
||||||
|
SCM Administrator
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="column is-align-self-center"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="column is-flex is-justify-content-flex-end is-align-items-center"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="ButtonAddons__Flex-sc-182golj-0 dcwwZy field has-addons m-0"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
className="control"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
aria-label="changeset.buttons.details"
|
||||||
|
className="button is-default is-reduced-mobile px-3"
|
||||||
|
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<i
|
||||||
|
className="fas fa-fw fa-exchange-alt has-text-inherit is-medium pr-5"
|
||||||
|
onKeyPress={[Function]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
changeset.buttons.details
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
className="control"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
aria-label="changeset.buttons.sources"
|
||||||
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -20493,7 +20875,7 @@ exports[`Storyshots Repositories/Changesets Replacements 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.details"
|
aria-label="changeset.buttons.details"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/changeset/d21cc6c359270aef2196796f4d96af65f51866dc"
|
href="/repo/hitchhiker/heartOfGold/code/changeset/d21cc6c359270aef2196796f4d96af65f51866dc"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -20513,7 +20895,7 @@ exports[`Storyshots Repositories/Changesets Replacements 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.sources"
|
aria-label="changeset.buttons.sources"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/sources/d21cc6c359270aef2196796f4d96af65f51866dc"
|
href="/repo/hitchhiker/heartOfGold/code/sources/d21cc6c359270aef2196796f4d96af65f51866dc"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -20620,7 +21002,7 @@ exports[`Storyshots Repositories/Changesets With Committer 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.details"
|
aria-label="changeset.buttons.details"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/changeset/d21cc6c359270aef2196796f4d96af65f51866dc"
|
href="/repo/hitchhiker/heartOfGold/code/changeset/d21cc6c359270aef2196796f4d96af65f51866dc"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -20640,7 +21022,7 @@ exports[`Storyshots Repositories/Changesets With Committer 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.sources"
|
aria-label="changeset.buttons.sources"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/sources/d21cc6c359270aef2196796f4d96af65f51866dc"
|
href="/repo/hitchhiker/heartOfGold/code/sources/d21cc6c359270aef2196796f4d96af65f51866dc"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -20756,7 +21138,7 @@ exports[`Storyshots Repositories/Changesets With Committer and Co-Author 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.details"
|
aria-label="changeset.buttons.details"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/changeset/a88567ef1e9528a700555cad8c4576b72fc7c6dd"
|
href="/repo/hitchhiker/heartOfGold/code/changeset/a88567ef1e9528a700555cad8c4576b72fc7c6dd"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -20776,7 +21158,7 @@ exports[`Storyshots Repositories/Changesets With Committer and Co-Author 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.sources"
|
aria-label="changeset.buttons.sources"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/sources/a88567ef1e9528a700555cad8c4576b72fc7c6dd"
|
href="/repo/hitchhiker/heartOfGold/code/sources/a88567ef1e9528a700555cad8c4576b72fc7c6dd"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -20884,7 +21266,7 @@ exports[`Storyshots Repositories/Changesets With avatar 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.details"
|
aria-label="changeset.buttons.details"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -20904,7 +21286,7 @@ exports[`Storyshots Repositories/Changesets With avatar 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.sources"
|
aria-label="changeset.buttons.sources"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -21008,7 +21390,7 @@ exports[`Storyshots Repositories/Changesets With contactless signature 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.details"
|
aria-label="changeset.buttons.details"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -21028,7 +21410,7 @@ exports[`Storyshots Repositories/Changesets With contactless signature 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.sources"
|
aria-label="changeset.buttons.sources"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -21132,7 +21514,7 @@ exports[`Storyshots Repositories/Changesets With invalid signature 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.details"
|
aria-label="changeset.buttons.details"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -21152,7 +21534,7 @@ exports[`Storyshots Repositories/Changesets With invalid signature 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.sources"
|
aria-label="changeset.buttons.sources"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -21260,7 +21642,7 @@ exports[`Storyshots Repositories/Changesets With multiple Co-Authors 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.details"
|
aria-label="changeset.buttons.details"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/changeset/b6c6f8fbd0d490936fae7d26ffdd4695cc2a0930"
|
href="/repo/hitchhiker/heartOfGold/code/changeset/b6c6f8fbd0d490936fae7d26ffdd4695cc2a0930"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -21280,7 +21662,7 @@ exports[`Storyshots Repositories/Changesets With multiple Co-Authors 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.sources"
|
aria-label="changeset.buttons.sources"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/sources/b6c6f8fbd0d490936fae7d26ffdd4695cc2a0930"
|
href="/repo/hitchhiker/heartOfGold/code/sources/b6c6f8fbd0d490936fae7d26ffdd4695cc2a0930"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -21384,7 +21766,7 @@ exports[`Storyshots Repositories/Changesets With multiple signatures and invalid
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.details"
|
aria-label="changeset.buttons.details"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -21404,7 +21786,7 @@ exports[`Storyshots Repositories/Changesets With multiple signatures and invalid
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.sources"
|
aria-label="changeset.buttons.sources"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -21508,7 +21890,7 @@ exports[`Storyshots Repositories/Changesets With multiple signatures and not fou
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.details"
|
aria-label="changeset.buttons.details"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -21528,7 +21910,7 @@ exports[`Storyshots Repositories/Changesets With multiple signatures and not fou
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.sources"
|
aria-label="changeset.buttons.sources"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -21632,7 +22014,7 @@ exports[`Storyshots Repositories/Changesets With multiple signatures and valid s
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.details"
|
aria-label="changeset.buttons.details"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -21652,7 +22034,7 @@ exports[`Storyshots Repositories/Changesets With multiple signatures and valid s
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.sources"
|
aria-label="changeset.buttons.sources"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -21756,7 +22138,7 @@ exports[`Storyshots Repositories/Changesets With unknown signature 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.details"
|
aria-label="changeset.buttons.details"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -21776,7 +22158,7 @@ exports[`Storyshots Repositories/Changesets With unknown signature 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.sources"
|
aria-label="changeset.buttons.sources"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -21880,7 +22262,7 @@ exports[`Storyshots Repositories/Changesets With unowned signature 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.details"
|
aria-label="changeset.buttons.details"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -21900,7 +22282,7 @@ exports[`Storyshots Repositories/Changesets With unowned signature 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.sources"
|
aria-label="changeset.buttons.sources"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -22004,7 +22386,7 @@ exports[`Storyshots Repositories/Changesets With valid signature 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.details"
|
aria-label="changeset.buttons.details"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -22024,7 +22406,7 @@ exports[`Storyshots Repositories/Changesets With valid signature 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="changeset.buttons.sources"
|
aria-label="changeset.buttons.sources"
|
||||||
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
|
className="button is-default is-reduced-mobile px-3"
|
||||||
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
* 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, { FC, MouseEvent, ReactNode, KeyboardEvent } from "react";
|
import React, { KeyboardEvent, MouseEvent, ReactNode, useCallback } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import Icon from "../Icon";
|
import Icon from "../Icon";
|
||||||
@@ -40,7 +40,6 @@ export type ButtonProps = {
|
|||||||
reducedMobile?: boolean;
|
reducedMobile?: boolean;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
testId?: string;
|
testId?: string;
|
||||||
ref?: React.ForwardedRef<HTMLButtonElement>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = ButtonProps & {
|
type Props = ButtonProps & {
|
||||||
@@ -48,85 +47,95 @@ type Props = ButtonProps & {
|
|||||||
color?: string;
|
color?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type InnerProps = Props & {
|
const Button = React.forwardRef<HTMLButtonElement | HTMLAnchorElement, Props>(
|
||||||
innerRef: React.Ref<HTMLButtonElement>;
|
(
|
||||||
};
|
{
|
||||||
|
link,
|
||||||
const Button: FC<InnerProps> = ({
|
className,
|
||||||
link,
|
icon,
|
||||||
className,
|
fullWidth,
|
||||||
icon,
|
reducedMobile,
|
||||||
fullWidth,
|
testId,
|
||||||
reducedMobile,
|
children,
|
||||||
testId,
|
label,
|
||||||
children,
|
type = "button",
|
||||||
label,
|
title,
|
||||||
type = "button",
|
loading,
|
||||||
title,
|
disabled,
|
||||||
loading,
|
action,
|
||||||
disabled,
|
color = "default",
|
||||||
action,
|
},
|
||||||
color = "default",
|
ref
|
||||||
innerRef
|
) => {
|
||||||
}) => {
|
const executeRef = useCallback(
|
||||||
const renderIcon = () => {
|
(el: HTMLButtonElement | HTMLAnchorElement | null) => {
|
||||||
return (
|
if (typeof ref === "function") {
|
||||||
<>
|
ref(el);
|
||||||
{icon ? (
|
} else if (ref) {
|
||||||
<Icon name={icon} color="inherit" className={classNames("is-medium", { "pr-5": label || children })} />
|
ref.current = el;
|
||||||
) : null}
|
}
|
||||||
</>
|
},
|
||||||
|
[ref]
|
||||||
);
|
);
|
||||||
};
|
const renderIcon = () => {
|
||||||
|
|
||||||
const classes = classNames(
|
|
||||||
"button",
|
|
||||||
"is-" + color,
|
|
||||||
{ "is-loading": loading },
|
|
||||||
{ "is-fullwidth": fullWidth },
|
|
||||||
{ "is-reduced-mobile": reducedMobile },
|
|
||||||
className
|
|
||||||
);
|
|
||||||
|
|
||||||
const content = (
|
|
||||||
<span>
|
|
||||||
{renderIcon()}{" "}
|
|
||||||
{(label || children) && (
|
|
||||||
<>
|
|
||||||
{label} {children}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (link && !disabled) {
|
|
||||||
if (link.includes("://")) {
|
|
||||||
return (
|
return (
|
||||||
<a className={classes} href={link} aria-label={label}>
|
<>
|
||||||
|
{icon ? (
|
||||||
|
<Icon name={icon} color="inherit" className={classNames("is-medium", { "pr-5": label || children })} />
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const classes = classNames(
|
||||||
|
"button",
|
||||||
|
"is-" + color,
|
||||||
|
{ "is-loading": loading },
|
||||||
|
{ "is-fullwidth": fullWidth },
|
||||||
|
{ "is-reduced-mobile": reducedMobile },
|
||||||
|
className
|
||||||
|
);
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<span>
|
||||||
|
{renderIcon()}{" "}
|
||||||
|
{(label || children) && (
|
||||||
|
<>
|
||||||
|
{label} {children}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (link && !disabled) {
|
||||||
|
if (link.includes("://")) {
|
||||||
|
return (
|
||||||
|
<a ref={executeRef} className={classes} href={link} aria-label={label}>
|
||||||
|
{content}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Link ref={executeRef} className={classes} to={link} aria-label={label}>
|
||||||
{content}
|
{content}
|
||||||
</a>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link className={classes} to={link} aria-label={label}>
|
<button
|
||||||
|
type={type}
|
||||||
|
title={title}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={(event) => action && action(event)}
|
||||||
|
className={classes}
|
||||||
|
ref={executeRef}
|
||||||
|
{...createAttributesForTesting(testId)}
|
||||||
|
>
|
||||||
{content}
|
{content}
|
||||||
</Link>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
export default Button;
|
||||||
<button
|
|
||||||
type={type}
|
|
||||||
title={title}
|
|
||||||
disabled={disabled}
|
|
||||||
onClick={event => action && action(event)}
|
|
||||||
className={classes}
|
|
||||||
ref={innerRef}
|
|
||||||
{...createAttributesForTesting(testId)}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default React.forwardRef<HTMLButtonElement, Props>((props, ref) => <Button {...props} innerRef={ref} />);
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
* 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, { ReactElement, ReactNode } from "react";
|
import React, { ReactElement, ReactNode, useEffect, useState } from "react";
|
||||||
import Button from "./Button";
|
import Button from "./Button";
|
||||||
import { storiesOf } from "@storybook/react";
|
import { storiesOf } from "@storybook/react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
@@ -85,3 +85,8 @@ buttonStory("DownloadButton", () => <DownloadButton displayName="Download" disab
|
|||||||
);
|
);
|
||||||
buttonStory("EditButton", () => <EditButton>Edit</EditButton>);
|
buttonStory("EditButton", () => <EditButton>Edit</EditButton>);
|
||||||
buttonStory("SubmitButton", () => <SubmitButton>Submit</SubmitButton>);
|
buttonStory("SubmitButton", () => <SubmitButton>Submit</SubmitButton>);
|
||||||
|
buttonStory("Button Ref", () => {
|
||||||
|
const [ref, setRef] = useState<HTMLButtonElement | HTMLAnchorElement | null>();
|
||||||
|
useEffect(() => ref?.focus(), [ref]);
|
||||||
|
return <Button ref={setRef}>Focus me!</Button>;
|
||||||
|
});
|
||||||
|
|||||||
@@ -37,16 +37,22 @@ export default function useRegisterModal(active: boolean, initialValue: boolean
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (active) {
|
if (active) {
|
||||||
previousActiveState.current = true;
|
previousActiveState.current = true;
|
||||||
increment();
|
if (increment) {
|
||||||
|
increment();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (previousActiveState.current !== null) {
|
if (previousActiveState.current !== null) {
|
||||||
decrement();
|
if (decrement) {
|
||||||
|
decrement();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
previousActiveState.current = false;
|
previousActiveState.current = false;
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
if (previousActiveState.current) {
|
if (previousActiveState.current) {
|
||||||
decrement();
|
if (decrement) {
|
||||||
|
decrement();
|
||||||
|
}
|
||||||
previousActiveState.current = null;
|
previousActiveState.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,12 +21,11 @@
|
|||||||
* 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, { FC } from "react";
|
import React from "react";
|
||||||
import { Changeset, File, Repository } from "@scm-manager/ui-types";
|
import { Changeset, File, Repository } from "@scm-manager/ui-types";
|
||||||
import { Button, ButtonAddons } from "../../buttons";
|
import { Button, ButtonAddons } from "../../buttons";
|
||||||
import { createChangesetLink, createSourcesLink } from "./changesets";
|
import { createChangesetLink, createSourcesLink } from "./changesets";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import styled from "styled-components";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
@@ -34,26 +33,31 @@ type Props = {
|
|||||||
file?: File;
|
file?: File;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SwitcherButton = styled(Button)`
|
const ChangesetButtonGroup = React.forwardRef<HTMLButtonElement | HTMLAnchorElement, Props>(
|
||||||
padding-right: 0.75rem;
|
({ repository, changeset, file }, ref) => {
|
||||||
padding-left: 0.75rem;
|
const [t] = useTranslation("repos");
|
||||||
`;
|
const changesetLink = createChangesetLink(repository, changeset);
|
||||||
|
const sourcesLink = createSourcesLink(repository, changeset, file);
|
||||||
const ChangesetButtonGroup: FC<Props> = ({ repository, changeset, file }) => {
|
return (
|
||||||
const [t] = useTranslation("repos");
|
<ButtonAddons className="m-0">
|
||||||
const changesetLink = createChangesetLink(repository, changeset);
|
<Button
|
||||||
const sourcesLink = createSourcesLink(repository, changeset, file);
|
className="px-3"
|
||||||
return (
|
ref={ref}
|
||||||
<ButtonAddons className="m-0">
|
link={changesetLink}
|
||||||
<SwitcherButton
|
icon="exchange-alt"
|
||||||
link={changesetLink}
|
label={t("changeset.buttons.details")}
|
||||||
icon="exchange-alt"
|
reducedMobile={true}
|
||||||
label={t("changeset.buttons.details")}
|
/>
|
||||||
reducedMobile={true}
|
<Button
|
||||||
/>
|
className="px-3"
|
||||||
<SwitcherButton link={sourcesLink} icon="code" label={t("changeset.buttons.sources")} reducedMobile={true} />
|
link={sourcesLink}
|
||||||
</ButtonAddons>
|
icon="code"
|
||||||
);
|
label={t("changeset.buttons.sources")}
|
||||||
};
|
reducedMobile={true}
|
||||||
|
/>
|
||||||
|
</ButtonAddons>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export default ChangesetButtonGroup;
|
export default ChangesetButtonGroup;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
import ChangesetRow from "./ChangesetRow";
|
import ChangesetRow from "./ChangesetRow";
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { Changeset, File, Repository } from "@scm-manager/ui-types";
|
import { Changeset, File, Repository } from "@scm-manager/ui-types";
|
||||||
|
import { KeyboardIterator } from "@scm-manager/ui-shortcuts";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
@@ -32,10 +33,13 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ChangesetList: FC<Props> = ({ repository, changesets, file }) => {
|
const ChangesetList: FC<Props> = ({ repository, changesets, file }) => {
|
||||||
const content = changesets.map(changeset => {
|
return (
|
||||||
return <ChangesetRow key={changeset.id} repository={repository} changeset={changeset} file={file} />;
|
<KeyboardIterator>
|
||||||
});
|
{changesets.map((changeset) => {
|
||||||
return <>{content}</>;
|
return <ChangesetRow key={changeset.id} repository={repository} changeset={changeset} file={file} />;
|
||||||
|
})}
|
||||||
|
</KeyboardIterator>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ChangesetList;
|
export default ChangesetList;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions";
|
|||||||
import { Changeset, File, Repository } from "@scm-manager/ui-types";
|
import { Changeset, File, Repository } from "@scm-manager/ui-types";
|
||||||
import ChangesetButtonGroup from "./ChangesetButtonGroup";
|
import ChangesetButtonGroup from "./ChangesetButtonGroup";
|
||||||
import SingleChangeset from "./SingleChangeset";
|
import SingleChangeset from "./SingleChangeset";
|
||||||
|
import { useKeyboardIteratorTarget } from "@scm-manager/ui-shortcuts";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
@@ -46,6 +47,7 @@ const Wrapper = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const ChangesetRow: FC<Props> = ({ repository, changeset, file }) => {
|
const ChangesetRow: FC<Props> = ({ repository, changeset, file }) => {
|
||||||
|
const ref = useKeyboardIteratorTarget();
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<div className={classNames("columns", "is-variable", "is-1-mobile", "is-0-tablet")}>
|
<div className={classNames("columns", "is-variable", "is-1-mobile", "is-0-tablet")}>
|
||||||
@@ -53,12 +55,12 @@ const ChangesetRow: FC<Props> = ({ repository, changeset, file }) => {
|
|||||||
<SingleChangeset repository={repository} changeset={changeset} />
|
<SingleChangeset repository={repository} changeset={changeset} />
|
||||||
</div>
|
</div>
|
||||||
<div className={classNames("column", "is-flex", "is-justify-content-flex-end", "is-align-items-center")}>
|
<div className={classNames("column", "is-flex", "is-justify-content-flex-end", "is-align-items-center")}>
|
||||||
<ChangesetButtonGroup repository={repository} changeset={changeset} file={file} />
|
<ChangesetButtonGroup ref={ref} repository={repository} changeset={changeset} file={file} />
|
||||||
<ExtensionPoint<extensionPoints.ChangesetRight>
|
<ExtensionPoint<extensionPoints.ChangesetRight>
|
||||||
name="changeset.right"
|
name="changeset.right"
|
||||||
props={{
|
props={{
|
||||||
repository,
|
repository,
|
||||||
changeset
|
changeset,
|
||||||
}}
|
}}
|
||||||
renderAll={true}
|
renderAll={true}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -28,13 +28,15 @@ import styled from "styled-components";
|
|||||||
import { MemoryRouter } from "react-router-dom";
|
import { MemoryRouter } from "react-router-dom";
|
||||||
import repository from "../../__resources__/repository";
|
import repository from "../../__resources__/repository";
|
||||||
import ChangesetRow from "./ChangesetRow";
|
import ChangesetRow from "./ChangesetRow";
|
||||||
import { one, two, three, four, five } from "../../__resources__/changesets";
|
import { five, four, one, three, two } from "../../__resources__/changesets";
|
||||||
import { Binder, BinderContext } from "@scm-manager/ui-extensions";
|
import { Binder, BinderContext } from "@scm-manager/ui-extensions";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import hitchhiker from "../../__resources__/hitchhiker.png";
|
import hitchhiker from "../../__resources__/hitchhiker.png";
|
||||||
import { Person } from "../../avatar/Avatar";
|
import { Person } from "../../avatar/Avatar";
|
||||||
import { Changeset } from "@scm-manager/ui-types";
|
import { Changeset } from "@scm-manager/ui-types";
|
||||||
import { Replacement } from "@scm-manager/ui-text";
|
import { Replacement } from "@scm-manager/ui-text";
|
||||||
|
import ChangesetList from "./ChangesetList";
|
||||||
|
import { ShortcutDocsContextProvider } from "@scm-manager/ui-shortcuts";
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
margin: 25rem 4rem;
|
margin: 25rem 4rem;
|
||||||
@@ -287,4 +289,9 @@ storiesOf("Repositories/Changesets", module)
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
return <ChangesetRow repository={repository} changeset={changeset} />;
|
return <ChangesetRow repository={repository} changeset={changeset} />;
|
||||||
});
|
})
|
||||||
|
.add("List with navigation", () => (
|
||||||
|
<ShortcutDocsContextProvider>
|
||||||
|
<ChangesetList repository={repository} changesets={[copy(one), copy(two), copy(three)]} />
|
||||||
|
</ShortcutDocsContextProvider>
|
||||||
|
));
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ describe("shortcutIterator", () => {
|
|||||||
expect(result.error).toBeUndefined();
|
expect(result.error).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call last callback upon pressing forward in initial state", async () => {
|
it("should call first callback upon pressing forward in initial state", async () => {
|
||||||
const callback = jest.fn();
|
const callback = jest.fn();
|
||||||
const callback2 = jest.fn();
|
const callback2 = jest.fn();
|
||||||
const callback3 = jest.fn();
|
const callback3 = jest.fn();
|
||||||
@@ -101,12 +101,12 @@ describe("shortcutIterator", () => {
|
|||||||
|
|
||||||
Mousetrap.trigger("j");
|
Mousetrap.trigger("j");
|
||||||
|
|
||||||
expect(callback).not.toHaveBeenCalled();
|
expect(callback).toHaveBeenCalledTimes(1);
|
||||||
expect(callback2).not.toHaveBeenCalled();
|
expect(callback2).not.toHaveBeenCalled();
|
||||||
expect(callback3).toHaveBeenCalledTimes(1);
|
expect(callback3).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call first callback once upon pressing backward in initial state", async () => {
|
it("should call last callback once upon pressing backward in initial state", async () => {
|
||||||
const callback = jest.fn();
|
const callback = jest.fn();
|
||||||
const callback2 = jest.fn();
|
const callback2 = jest.fn();
|
||||||
const callback3 = jest.fn();
|
const callback3 = jest.fn();
|
||||||
@@ -117,12 +117,11 @@ describe("shortcutIterator", () => {
|
|||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
|
|
||||||
Mousetrap.trigger("k");
|
|
||||||
Mousetrap.trigger("k");
|
Mousetrap.trigger("k");
|
||||||
|
|
||||||
expect(callback).toHaveBeenCalledTimes(1);
|
expect(callback).not.toHaveBeenCalled();
|
||||||
expect(callback2).not.toHaveBeenCalled();
|
expect(callback2).not.toHaveBeenCalled();
|
||||||
expect(callback3).not.toHaveBeenCalled();
|
expect(callback3).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not allow moving past the end of the callback array", async () => {
|
it("should not allow moving past the end of the callback array", async () => {
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export const KeyboardIteratorContextProvider: FC<{ initialIndex?: number }> = ({
|
|||||||
|
|
||||||
const navigateBackward = useCallback(() => {
|
const navigateBackward = useCallback(() => {
|
||||||
if (activeIndex.current === -1) {
|
if (activeIndex.current === -1) {
|
||||||
activeIndex.current = 0;
|
activeIndex.current = callbacks.current.length - 1;
|
||||||
executeCallback(activeIndex.current);
|
executeCallback(activeIndex.current);
|
||||||
} else if (activeIndex.current > 0) {
|
} else if (activeIndex.current > 0) {
|
||||||
activeIndex.current -= 1;
|
activeIndex.current -= 1;
|
||||||
@@ -67,7 +67,7 @@ export const KeyboardIteratorContextProvider: FC<{ initialIndex?: number }> = ({
|
|||||||
|
|
||||||
const navigateForward = useCallback(() => {
|
const navigateForward = useCallback(() => {
|
||||||
if (activeIndex.current === -1) {
|
if (activeIndex.current === -1) {
|
||||||
activeIndex.current = callbacks.current.length - 1;
|
activeIndex.current = 0;
|
||||||
executeCallback(activeIndex.current);
|
executeCallback(activeIndex.current);
|
||||||
} else if (activeIndex.current < callbacks.current.length - 1) {
|
} else if (activeIndex.current < callbacks.current.length - 1) {
|
||||||
activeIndex.current += 1;
|
activeIndex.current += 1;
|
||||||
|
|||||||
@@ -537,19 +537,11 @@ ul.is-separated {
|
|||||||
}
|
}
|
||||||
.content.is-plugin-page {
|
.content.is-plugin-page {
|
||||||
.card-columns {
|
.card-columns {
|
||||||
.column.is-half .overlay-column {
|
.column .overlay-column {
|
||||||
width: calc(37.5% - 1.5rem);
|
left: 0.5rem;
|
||||||
}
|
top: 0.5rem;
|
||||||
|
width: calc(100% - 1rem);
|
||||||
.column.is-full .overlay-column {
|
height: calc(160px - 1rem);
|
||||||
width: calc(75% - 1.5rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
|
||||||
.column.is-half .overlay-column,
|
|
||||||
.column.is-full .overlay-column {
|
|
||||||
width: calc(100% - 1.5rem);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import PluginAvatar from "./PluginAvatar";
|
import PluginAvatar from "./PluginAvatar";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import MyCloudoguTag from "./MyCloudoguTag";
|
import MyCloudoguTag from "./MyCloudoguTag";
|
||||||
|
import { useKeyboardIteratorTarget } from "@scm-manager/ui-shortcuts";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
plugin: Plugin;
|
plugin: Plugin;
|
||||||
@@ -43,8 +44,8 @@ const ActionbarWrapper = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const IconWrapperStyle = styled.span.attrs(props => ({
|
const IconWrapperStyle = styled.span.attrs((props) => ({
|
||||||
className: "level-item mb-0 p-2 is-clickable"
|
className: "level-item mb-0 p-2 is-clickable",
|
||||||
}))`
|
}))`
|
||||||
border: 1px solid #cdcdcd; // $dark-25
|
border: 1px solid #cdcdcd; // $dark-25
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -56,7 +57,7 @@ const IconWrapperStyle = styled.span.attrs(props => ({
|
|||||||
|
|
||||||
const IconWrapper: FC<{ action: () => void }> = ({ action, children }) => {
|
const IconWrapper: FC<{ action: () => void }> = ({ action, children }) => {
|
||||||
return (
|
return (
|
||||||
<IconWrapperStyle onClick={action} onKeyDown={e => e.key === "Enter" && action()} tabIndex={0}>
|
<IconWrapperStyle onClick={action} onKeyDown={(e) => e.key === "Enter" && action()} tabIndex={0}>
|
||||||
{children}
|
{children}
|
||||||
</IconWrapperStyle>
|
</IconWrapperStyle>
|
||||||
);
|
);
|
||||||
@@ -69,6 +70,7 @@ const PluginEntry: FC<Props> = ({ plugin, openModal, pluginCenterAuthInfo }) =>
|
|||||||
const isUninstallable = plugin._links.uninstall && (plugin._links.uninstall as Link).href;
|
const isUninstallable = plugin._links.uninstall && (plugin._links.uninstall as Link).href;
|
||||||
const isCloudoguPlugin = plugin.type === "CLOUDOGU";
|
const isCloudoguPlugin = plugin.type === "CLOUDOGU";
|
||||||
const isDefaultPluginCenterLoginAvailable = pluginCenterAuthInfo?.default && !!pluginCenterAuthInfo?._links?.login;
|
const isDefaultPluginCenterLoginAvailable = pluginCenterAuthInfo?.default && !!pluginCenterAuthInfo?._links?.login;
|
||||||
|
const ref = useKeyboardIteratorTarget();
|
||||||
|
|
||||||
const evaluateAction = () => {
|
const evaluateAction = () => {
|
||||||
if (isInstallable) {
|
if (isInstallable) {
|
||||||
@@ -118,6 +120,7 @@ const PluginEntry: FC<Props> = ({ plugin, openModal, pluginCenterAuthInfo }) =>
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CardColumn
|
<CardColumn
|
||||||
|
ref={ref}
|
||||||
action={evaluateAction()}
|
action={evaluateAction()}
|
||||||
avatar={<PluginAvatar plugin={plugin} />}
|
avatar={<PluginAvatar plugin={plugin} />}
|
||||||
title={plugin.displayName ? <strong>{plugin.displayName}</strong> : <strong>{plugin.name}</strong>}
|
title={plugin.displayName ? <strong>{plugin.displayName}</strong> : <strong>{plugin.name}</strong>}
|
||||||
@@ -129,7 +132,7 @@ const PluginEntry: FC<Props> = ({ plugin, openModal, pluginCenterAuthInfo }) =>
|
|||||||
<div
|
<div
|
||||||
className={classNames("is-flex", {
|
className={classNames("is-flex", {
|
||||||
"is-justify-content-space-between": isCloudoguPlugin,
|
"is-justify-content-space-between": isCloudoguPlugin,
|
||||||
"is-justify-content-end": !isCloudoguPlugin
|
"is-justify-content-end": !isCloudoguPlugin,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{isCloudoguPlugin ? <MyCloudoguTag /> : null}
|
{isCloudoguPlugin ? <MyCloudoguTag /> : null}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { Plugin, PluginCenterAuthenticationInfo } from "@scm-manager/ui-types";
|
|||||||
import PluginGroupEntry from "../components/PluginGroupEntry";
|
import PluginGroupEntry from "../components/PluginGroupEntry";
|
||||||
import groupByCategory from "./groupByCategory";
|
import groupByCategory from "./groupByCategory";
|
||||||
import { PluginModalContent } from "../containers/PluginsOverview";
|
import { PluginModalContent } from "../containers/PluginsOverview";
|
||||||
|
import { KeyboardIterator } from "@scm-manager/ui-shortcuts";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
plugins: Plugin[];
|
plugins: Plugin[];
|
||||||
@@ -37,16 +38,18 @@ const PluginList: FC<Props> = ({ plugins, openModal, pluginCenterAuthInfo }) =>
|
|||||||
const groups = groupByCategory(plugins);
|
const groups = groupByCategory(plugins);
|
||||||
return (
|
return (
|
||||||
<div className="content is-plugin-page">
|
<div className="content is-plugin-page">
|
||||||
{groups.map(group => {
|
<KeyboardIterator>
|
||||||
return (
|
{groups.map((group) => {
|
||||||
<PluginGroupEntry
|
return (
|
||||||
group={group}
|
<PluginGroupEntry
|
||||||
openModal={openModal}
|
group={group}
|
||||||
key={group.name}
|
openModal={openModal}
|
||||||
pluginCenterAuthInfo={pluginCenterAuthInfo}
|
key={group.name}
|
||||||
/>
|
pluginCenterAuthInfo={pluginCenterAuthInfo}
|
||||||
);
|
/>
|
||||||
})}
|
);
|
||||||
|
})}
|
||||||
|
</KeyboardIterator>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,40 +21,41 @@
|
|||||||
* 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 { WithTranslation, withTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { Group } from "@scm-manager/ui-types";
|
import { Group } from "@scm-manager/ui-types";
|
||||||
import { Icon } from "@scm-manager/ui-components";
|
import { Icon } from "@scm-manager/ui-components";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { useKeyboardIteratorTarget } from "@scm-manager/ui-shortcuts";
|
||||||
|
|
||||||
type Props = WithTranslation & {
|
type Props = {
|
||||||
group: Group;
|
group: Group;
|
||||||
};
|
};
|
||||||
|
|
||||||
class GroupRow extends React.Component<Props> {
|
const GroupRow: FC<Props> = ({ group }) => {
|
||||||
renderLink(to: string, label: string) {
|
const ref = useKeyboardIteratorTarget();
|
||||||
return <Link to={to}>{label}</Link>;
|
const [t] = useTranslation("groups");
|
||||||
}
|
const to = `/group/${group.name}`;
|
||||||
|
const iconType = group.external ? (
|
||||||
|
<Icon title={t("group.external")} name="globe-americas" />
|
||||||
|
) : (
|
||||||
|
<Icon title={t("group.internal")} name="home" />
|
||||||
|
);
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
const { group, t } = this.props;
|
<tr>
|
||||||
const to = `/group/${group.name}`;
|
<td className="is-word-break">
|
||||||
const iconType = group.external ? (
|
{iconType}{" "}
|
||||||
<Icon title={t("group.external")} name="globe-americas" />
|
{
|
||||||
) : (
|
<Link ref={ref} to={to}>
|
||||||
<Icon title={t("group.internal")} name="home" />
|
{group.name}
|
||||||
);
|
</Link>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td className={classNames("is-hidden-mobile", "is-word-break")}>{group.description}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
export default GroupRow;
|
||||||
<tr>
|
|
||||||
<td className="is-word-break">
|
|
||||||
{iconType} {this.renderLink(to, group.name)}
|
|
||||||
</td>
|
|
||||||
<td className={classNames("is-hidden-mobile", "is-word-break")}>{group.description}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withTranslation("groups")(GroupRow);
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import React, { FC } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Group } from "@scm-manager/ui-types";
|
import { Group } from "@scm-manager/ui-types";
|
||||||
import GroupRow from "./GroupRow";
|
import GroupRow from "./GroupRow";
|
||||||
|
import { KeyboardIterator } from "@scm-manager/ui-shortcuts";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
groups: Group[];
|
groups: Group[];
|
||||||
@@ -34,19 +35,21 @@ const GroupTable: FC<Props> = ({ groups }) => {
|
|||||||
const [t] = useTranslation("groups");
|
const [t] = useTranslation("groups");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table className="card-table table is-hoverable is-fullwidth">
|
<KeyboardIterator>
|
||||||
<thead>
|
<table className="card-table table is-hoverable is-fullwidth">
|
||||||
<tr>
|
<thead>
|
||||||
<th>{t("group.name")}</th>
|
<tr>
|
||||||
<th className="is-hidden-mobile">{t("group.description")}</th>
|
<th>{t("group.name")}</th>
|
||||||
</tr>
|
<th className="is-hidden-mobile">{t("group.description")}</th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
</thead>
|
||||||
{groups.map((group, index) => {
|
<tbody>
|
||||||
return <GroupRow key={index} group={group} />;
|
{groups.map((group, index) => {
|
||||||
})}
|
return <GroupRow key={index} group={group} />;
|
||||||
</tbody>
|
})}
|
||||||
</table>
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</KeyboardIterator>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { binder } from "@scm-manager/ui-extensions";
|
|||||||
import DefaultBranchTag from "./DefaultBranchTag";
|
import DefaultBranchTag from "./DefaultBranchTag";
|
||||||
import AheadBehindTag from "./AheadBehindTag";
|
import AheadBehindTag from "./AheadBehindTag";
|
||||||
import BranchCommitDateCommitter from "./BranchCommitDateCommitter";
|
import BranchCommitDateCommitter from "./BranchCommitDateCommitter";
|
||||||
|
import { useKeyboardIteratorTarget } from "@scm-manager/ui-shortcuts";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
@@ -64,6 +65,7 @@ const MobileFlowSpan = styled.span`
|
|||||||
const BranchRow: FC<Props> = ({ repository, baseUrl, branch, onDelete, details }) => {
|
const BranchRow: FC<Props> = ({ repository, baseUrl, branch, onDelete, details }) => {
|
||||||
const to = `${baseUrl}/${encodeURIComponent(branch.name)}/info`;
|
const to = `${baseUrl}/${encodeURIComponent(branch.name)}/info`;
|
||||||
const [t] = useTranslation("repos");
|
const [t] = useTranslation("repos");
|
||||||
|
const ref = useKeyboardIteratorTarget();
|
||||||
|
|
||||||
let deleteButton;
|
let deleteButton;
|
||||||
if ((branch?._links?.delete as Link)?.href) {
|
if ((branch?._links?.delete as Link)?.href) {
|
||||||
@@ -92,7 +94,7 @@ const BranchRow: FC<Props> = ({ repository, baseUrl, branch, onDelete, details }
|
|||||||
return (
|
return (
|
||||||
<AdaptTableFlow>
|
<AdaptTableFlow>
|
||||||
<td className="is-vertical-align-middle">
|
<td className="is-vertical-align-middle">
|
||||||
<ReactLink to={to} title={branch.name}>
|
<ReactLink ref={ref} to={to} title={branch.name}>
|
||||||
{branch.name}
|
{branch.name}
|
||||||
</ReactLink>
|
</ReactLink>
|
||||||
{branch.lastCommitDate && (
|
{branch.lastCommitDate && (
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { orderBranches } from "../util/orderBranches";
|
|||||||
import BranchTable from "../components/BranchTable";
|
import BranchTable from "../components/BranchTable";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useBranchDetailsCollection } from "@scm-manager/ui-api";
|
import { useBranchDetailsCollection } from "@scm-manager/ui-api";
|
||||||
|
import { KeyboardIterator } from "@scm-manager/ui-shortcuts";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
@@ -40,11 +41,11 @@ const BranchTableWrapper: FC<Props> = ({ repository, baseUrl, data }) => {
|
|||||||
const [t] = useTranslation("repos");
|
const [t] = useTranslation("repos");
|
||||||
const branches: Branch[] = (data?._embedded?.branches as Branch[]) || [];
|
const branches: Branch[] = (data?._embedded?.branches as Branch[]) || [];
|
||||||
orderBranches(branches);
|
orderBranches(branches);
|
||||||
const staleBranches = branches.filter(b => b.stale);
|
const staleBranches = branches.filter((b) => b.stale);
|
||||||
const activeBranches = branches.filter(b => !b.stale);
|
const activeBranches = branches.filter((b) => !b.stale);
|
||||||
const { error, data: branchesDetails } = useBranchDetailsCollection(repository, [
|
const { error, data: branchesDetails } = useBranchDetailsCollection(repository, [
|
||||||
...activeBranches,
|
...activeBranches,
|
||||||
...staleBranches
|
...staleBranches,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (branches.length === 0) {
|
if (branches.length === 0) {
|
||||||
@@ -54,7 +55,7 @@ const BranchTableWrapper: FC<Props> = ({ repository, baseUrl, data }) => {
|
|||||||
const showCreateButton = !!data._links.create;
|
const showCreateButton = !!data._links.create;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<KeyboardIterator>
|
||||||
<Subtitle subtitle={t("branches.overview.title")} />
|
<Subtitle subtitle={t("branches.overview.title")} />
|
||||||
<ErrorNotification error={error} />
|
<ErrorNotification error={error} />
|
||||||
{activeBranches.length > 0 ? (
|
{activeBranches.length > 0 ? (
|
||||||
@@ -76,7 +77,7 @@ const BranchTableWrapper: FC<Props> = ({ repository, baseUrl, data }) => {
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{showCreateButton ? <CreateButton label={t("branches.overview.createButton")} link="./create" /> : null}
|
{showCreateButton ? <CreateButton label={t("branches.overview.createButton")} link="./create" /> : null}
|
||||||
</>
|
</KeyboardIterator>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { File, Repository } from "@scm-manager/ui-types";
|
|||||||
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 { KeyboardIterator } from "@scm-manager/ui-shortcuts";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
@@ -69,8 +70,8 @@ const FileTree: FC<Props> = ({ repository, directory, baseUrl, revision, fetchNe
|
|||||||
revision,
|
revision,
|
||||||
_links: {},
|
_links: {},
|
||||||
_embedded: {
|
_embedded: {
|
||||||
children: []
|
children: [],
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +83,7 @@ const FileTree: FC<Props> = ({ repository, directory, baseUrl, revision, fetchNe
|
|||||||
repository,
|
repository,
|
||||||
directory,
|
directory,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
revision
|
revision,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -105,9 +106,11 @@ const FileTree: FC<Props> = ({ repository, directory, baseUrl, revision, fetchNe
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{files.map((file: File) => (
|
<KeyboardIterator>
|
||||||
<FileTreeLeaf key={file.name} file={file} baseUrl={baseUrlWithRevision} repository={repository} />
|
{files.map((file: File) => (
|
||||||
))}
|
<FileTreeLeaf key={file.name} file={file} baseUrl={baseUrlWithRevision} repository={repository} />
|
||||||
|
))}
|
||||||
|
</KeyboardIterator>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<TruncatedNotification
|
<TruncatedNotification
|
||||||
|
|||||||
@@ -22,15 +22,16 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { FC, ReactElement } 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, extensionPoints } from "@scm-manager/ui-extensions";
|
import { binder, ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions";
|
||||||
import { File, Repository } from "@scm-manager/ui-types";
|
import { File, Repository } from "@scm-manager/ui-types";
|
||||||
import { devices, DateFromNow, FileSize, Icon, Tooltip } from "@scm-manager/ui-components";
|
import { DateFromNow, devices, FileSize, Icon, Tooltip } from "@scm-manager/ui-components";
|
||||||
import FileIcon from "./FileIcon";
|
import FileIcon from "./FileIcon";
|
||||||
import FileLink from "./content/FileLink";
|
import FileLink from "./content/FileLink";
|
||||||
import { ReactElement } from "react";
|
import { useKeyboardIteratorTarget } from "@scm-manager/ui-shortcuts";
|
||||||
|
|
||||||
type Props = WithTranslation & {
|
type Props = WithTranslation & {
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
@@ -58,6 +59,15 @@ const ExtensionTd = styled.td`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const FileName: FC<{ file: File; baseUrl: string }> = ({ file, baseUrl }) => {
|
||||||
|
const ref = useKeyboardIteratorTarget();
|
||||||
|
return (
|
||||||
|
<FileLink ref={ref} baseUrl={baseUrl} file={file} tabIndex={0}>
|
||||||
|
{file.name}
|
||||||
|
</FileLink>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
class FileTreeLeaf extends React.Component<Props> {
|
class FileTreeLeaf extends React.Component<Props> {
|
||||||
createFileIcon = (file: File) => {
|
createFileIcon = (file: File) => {
|
||||||
return (
|
return (
|
||||||
@@ -67,14 +77,6 @@ class FileTreeLeaf extends React.Component<Props> {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
createFileName = (file: File) => {
|
|
||||||
return (
|
|
||||||
<FileLink baseUrl={this.props.baseUrl} file={file} tabIndex={0}>
|
|
||||||
{file.name}
|
|
||||||
</FileLink>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
contentIfPresent = (file: File, attribute: string, content: (file: File) => ReactElement | string | undefined) => {
|
contentIfPresent = (file: File, attribute: string, content: (file: File) => ReactElement | string | undefined) => {
|
||||||
const { t } = this.props;
|
const { t } = this.props;
|
||||||
if (file.hasOwnProperty(attribute)) {
|
if (file.hasOwnProperty(attribute)) {
|
||||||
@@ -97,27 +99,29 @@ class FileTreeLeaf extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { repository, file } = this.props;
|
const { repository, file, baseUrl } = this.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 = {
|
const extProps: extensionPoints.ReposSourcesTreeRowProps = {
|
||||||
repository,
|
repository,
|
||||||
file
|
file,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{this.createFileIcon(file)}</td>
|
<td>{this.createFileIcon(file)}</td>
|
||||||
<MinWidthTd className="is-word-break">{this.createFileName(file)}</MinWidthTd>
|
<MinWidthTd className="is-word-break">
|
||||||
|
<FileName file={file} baseUrl={baseUrl} />
|
||||||
|
</MinWidthTd>
|
||||||
<NoWrapTd className="is-hidden-mobile">
|
<NoWrapTd className="is-hidden-mobile">
|
||||||
{file.directory ? "" : this.contentIfPresent(file, "length", renderFileSize)}
|
{file.directory ? "" : this.contentIfPresent(file, "length", renderFileSize)}
|
||||||
</NoWrapTd>
|
</NoWrapTd>
|
||||||
<td className="is-hidden-mobile">{this.contentIfPresent(file, "commitDate", renderCommitDate)}</td>
|
<td className="is-hidden-mobile">{this.contentIfPresent(file, "commitDate", renderCommitDate)}</td>
|
||||||
<MinWidthTd className={classNames("is-word-break", "is-hidden-touch")}>
|
<MinWidthTd className={classNames("is-word-break", "is-hidden-touch")}>
|
||||||
{this.contentIfPresent(file, "description", file => file.description)}
|
{this.contentIfPresent(file, "description", (file) => file.description)}
|
||||||
</MinWidthTd>
|
</MinWidthTd>
|
||||||
|
|
||||||
{binder.hasExtension<extensionPoints.ReposSourcesTreeRowRight>("repos.sources.tree.row.right", extProps) && (
|
{binder.hasExtension<extensionPoints.ReposSourcesTreeRowRight>("repos.sources.tree.row.right", extProps) && (
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
* 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, { FC, ReactNode } from "react";
|
import React, { ReactNode } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { File } from "@scm-manager/ui-types";
|
import { File } from "@scm-manager/ui-types";
|
||||||
@@ -63,10 +63,7 @@ export const createRelativeLink = (repositoryUrl: string) => {
|
|||||||
export const createFolderLink = (base: string, file: File) => {
|
export const createFolderLink = (base: string, file: File) => {
|
||||||
let link = base;
|
let link = base;
|
||||||
if (file.path) {
|
if (file.path) {
|
||||||
let path = file.path
|
let path = file.path.split("/").map(encodePart).join("/");
|
||||||
.split("/")
|
|
||||||
.map(encodePart)
|
|
||||||
.join("/");
|
|
||||||
if (path.startsWith("/")) {
|
if (path.startsWith("/")) {
|
||||||
path = path.substring(1);
|
path = path.substring(1);
|
||||||
}
|
}
|
||||||
@@ -78,7 +75,7 @@ export const createFolderLink = (base: string, file: File) => {
|
|||||||
return link;
|
return link;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FileLink: FC<Props> = ({ baseUrl, file, children, tabIndex }) => {
|
const FileLink = React.forwardRef<HTMLAnchorElement, Props>(({ baseUrl, file, children, tabIndex }, ref) => {
|
||||||
const [t] = useTranslation("repos");
|
const [t] = useTranslation("repos");
|
||||||
if (file?.subRepository?.repositoryUrl) {
|
if (file?.subRepository?.repositoryUrl) {
|
||||||
// file link represents a subRepository
|
// file link represents a subRepository
|
||||||
@@ -117,10 +114,10 @@ const FileLink: FC<Props> = ({ baseUrl, file, children, tabIndex }) => {
|
|||||||
}
|
}
|
||||||
// normal file or folder
|
// normal file or folder
|
||||||
return (
|
return (
|
||||||
<Link to={createFolderLink(baseUrl, file)} tabIndex={tabIndex}>
|
<Link ref={ref} to={createFolderLink(baseUrl, file)} tabIndex={tabIndex}>
|
||||||
{children}
|
{children}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export default FileLink;
|
export default FileLink;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Tag, Link } from "@scm-manager/ui-types";
|
import { Tag, Link } from "@scm-manager/ui-types";
|
||||||
import { Button, DateFromNow } from "@scm-manager/ui-components";
|
import { Button, DateFromNow } from "@scm-manager/ui-components";
|
||||||
|
import { useKeyboardIteratorTarget } from "@scm-manager/ui-shortcuts";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
tag: Tag;
|
tag: Tag;
|
||||||
@@ -37,6 +38,7 @@ type Props = {
|
|||||||
|
|
||||||
const TagRow: FC<Props> = ({ tag, baseUrl, onDelete }) => {
|
const TagRow: FC<Props> = ({ tag, baseUrl, onDelete }) => {
|
||||||
const [t] = useTranslation("repos");
|
const [t] = useTranslation("repos");
|
||||||
|
const ref = useKeyboardIteratorTarget();
|
||||||
|
|
||||||
let deleteButton;
|
let deleteButton;
|
||||||
if ((tag?._links?.delete as Link)?.href) {
|
if ((tag?._links?.delete as Link)?.href) {
|
||||||
@@ -49,7 +51,7 @@ const TagRow: FC<Props> = ({ tag, baseUrl, onDelete }) => {
|
|||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td className="is-vertical-align-middle">
|
<td className="is-vertical-align-middle">
|
||||||
<RouterLink to={to} title={tag.name}>
|
<RouterLink ref={ref} to={to} title={tag.name}>
|
||||||
{tag.name}
|
{tag.name}
|
||||||
<span className={classNames("has-text-secondary", "is-ellipsis-overflow", "ml-2", "is-size-7")}>
|
<span className={classNames("has-text-secondary", "is-ellipsis-overflow", "ml-2", "is-size-7")}>
|
||||||
{t("tags.overview.created")} <DateFromNow date={tag.date} />
|
{t("tags.overview.created")} <DateFromNow date={tag.date} />
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import TagRow from "./TagRow";
|
import TagRow from "./TagRow";
|
||||||
import { ConfirmAlert, ErrorNotification } from "@scm-manager/ui-components";
|
import { ConfirmAlert, ErrorNotification } from "@scm-manager/ui-components";
|
||||||
import { useDeleteTag } from "@scm-manager/ui-api";
|
import { useDeleteTag } from "@scm-manager/ui-api";
|
||||||
|
import { KeyboardIterator } from "@scm-manager/ui-shortcuts";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
@@ -76,14 +77,14 @@ const TagTable: FC<Props> = ({ repository, baseUrl, tags }) => {
|
|||||||
{
|
{
|
||||||
label: t("tag.delete.confirmAlert.submit"),
|
label: t("tag.delete.confirmAlert.submit"),
|
||||||
isLoading,
|
isLoading,
|
||||||
onClick: () => deleteTag()
|
onClick: () => deleteTag(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
className: "is-info",
|
className: "is-info",
|
||||||
label: t("tag.delete.confirmAlert.cancel"),
|
label: t("tag.delete.confirmAlert.cancel"),
|
||||||
onClick: () => abortDelete(),
|
onClick: () => abortDelete(),
|
||||||
autofocus: true
|
autofocus: true,
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
close={() => abortDelete()}
|
close={() => abortDelete()}
|
||||||
/>
|
/>
|
||||||
@@ -96,9 +97,11 @@ const TagTable: FC<Props> = ({ repository, baseUrl, tags }) => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{tags.map(tag => (
|
<KeyboardIterator>
|
||||||
<TagRow key={tag.name} baseUrl={baseUrl} tag={tag} onDelete={onDelete} />
|
{tags.map((tag) => (
|
||||||
))}
|
<TagRow key={tag.name} baseUrl={baseUrl} tag={tag} onDelete={onDelete} />
|
||||||
|
))}
|
||||||
|
</KeyboardIterator>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -21,47 +21,49 @@
|
|||||||
* 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 { WithTranslation, withTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { User } from "@scm-manager/ui-types";
|
import { User } from "@scm-manager/ui-types";
|
||||||
import { createAttributesForTesting, Icon } from "@scm-manager/ui-components";
|
import { createAttributesForTesting, Icon } from "@scm-manager/ui-components";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { useKeyboardIteratorTarget } from "@scm-manager/ui-shortcuts";
|
||||||
|
|
||||||
type Props = WithTranslation & {
|
type Props = {
|
||||||
user: User;
|
user: User;
|
||||||
};
|
};
|
||||||
|
|
||||||
class UserRow extends React.Component<Props> {
|
const UserRowLink = React.forwardRef<HTMLAnchorElement, { to: string; children: string }>(({ children, to }, ref) => (
|
||||||
renderLink(to: string, label: string) {
|
<Link ref={ref} to={to} {...createAttributesForTesting(children)}>
|
||||||
return (
|
{children}
|
||||||
<Link to={to} {...createAttributesForTesting(label)}>
|
</Link>
|
||||||
{label}
|
));
|
||||||
</Link>
|
const UserRow: FC<Props> = ({ user }) => {
|
||||||
);
|
const ref = useKeyboardIteratorTarget();
|
||||||
}
|
const [t] = useTranslation("users");
|
||||||
|
const to = `/user/${user.name}`;
|
||||||
|
const iconType = user.active ? (
|
||||||
|
<Icon title={t("user.active")} name="user" />
|
||||||
|
) : (
|
||||||
|
<Icon title={t("user.inactive")} name="user-slash" />
|
||||||
|
);
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
const { user, t } = this.props;
|
<tr className={user.active ? "border-is-green" : "border-is-yellow"}>
|
||||||
const to = `/user/${user.name}`;
|
<td className="is-word-break">
|
||||||
const iconType = user.active ? (
|
{iconType}{" "}
|
||||||
<Icon title={t("user.active")} name="user" />
|
<UserRowLink ref={ref} to={to}>
|
||||||
) : (
|
{user.name}
|
||||||
<Icon title={t("user.inactive")} name="user-slash" />
|
</UserRowLink>
|
||||||
);
|
</td>
|
||||||
|
<td className={classNames("is-hidden-mobile", "is-word-break")}>
|
||||||
|
<UserRowLink to={to}>{user.displayName}</UserRowLink>
|
||||||
|
</td>
|
||||||
|
<td className={classNames("is-hidden-mobile", "is-word-break")}>
|
||||||
|
{user.mail ? <a href={`mailto:${user.mail}`}>{user.mail}</a> : null}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
export default UserRow;
|
||||||
<tr className={user.active ? "border-is-green" : "border-is-yellow"}>
|
|
||||||
<td className="is-word-break">
|
|
||||||
{iconType} {this.renderLink(to, user.name)}
|
|
||||||
</td>
|
|
||||||
<td className={classNames("is-hidden-mobile", "is-word-break")}>{this.renderLink(to, user.displayName)}</td>
|
|
||||||
<td className={classNames("is-hidden-mobile", "is-word-break")}>
|
|
||||||
{user.mail ? <a href={`mailto:${user.mail}`}>{user.mail}</a> : null}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withTranslation("users")(UserRow);
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import React, { FC } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { User } from "@scm-manager/ui-types";
|
import { User } from "@scm-manager/ui-types";
|
||||||
import UserRow from "./UserRow";
|
import UserRow from "./UserRow";
|
||||||
|
import { KeyboardIterator } from "@scm-manager/ui-shortcuts";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
users: User[];
|
users: User[];
|
||||||
@@ -34,20 +35,22 @@ const UserTable: FC<Props> = ({ users }) => {
|
|||||||
const [t] = useTranslation("users");
|
const [t] = useTranslation("users");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table className="card-table table is-hoverable is-fullwidth">
|
<KeyboardIterator>
|
||||||
<thead>
|
<table className="card-table table is-hoverable is-fullwidth">
|
||||||
<tr>
|
<thead>
|
||||||
<th>{t("user.name")}</th>
|
<tr>
|
||||||
<th className="is-hidden-mobile">{t("user.displayName")}</th>
|
<th>{t("user.name")}</th>
|
||||||
<th className="is-hidden-mobile">{t("user.mail")}</th>
|
<th className="is-hidden-mobile">{t("user.displayName")}</th>
|
||||||
</tr>
|
<th className="is-hidden-mobile">{t("user.mail")}</th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
</thead>
|
||||||
{users.map((user, index) => {
|
<tbody>
|
||||||
return <UserRow key={index} user={user} />;
|
{users.map((user, index) => {
|
||||||
})}
|
return <UserRow key={index} user={user} />;
|
||||||
</tbody>
|
})}
|
||||||
</table>
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</KeyboardIterator>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user