Accessible Tabpanel

Pushed-by: Florian Scholdei<florian.scholdei@cloudogu.com>
Co-authored-by: Florian Scholdei<florian.scholdei@cloudogu.com>
Pushed-by: Konstantin Schaper<konstantin.schaper@cloudogu.com>
Co-authored-by: Konstantin Schaper<konstantin.schaper@cloudogu.com>
Committed-by: Florian Scholdei<florian.scholdei@cloudogu.com>
This commit is contained in:
Laura Gorzitze
2023-09-27 09:16:22 +02:00
parent 4033f71bfb
commit 0d93975715
17 changed files with 555 additions and 289 deletions

View File

@@ -0,0 +1,4 @@
- type: fixed
description: Make compare accessible
- type: fixed
description: Compare target when default branch contains slash

View File

@@ -42,6 +42,7 @@ export { default as ConfigurationForm } from "./ConfigurationForm";
export { default as SelectField } from "./select/SelectField";
export { default as ChipInputField } from "./chip-input/ChipInputField";
export { default as ComboboxField } from "./combobox/ComboboxField";
export { default as Input } from "./input/Input";
export { default as Select } from "./select/Select";
export * from "./resourceHooks";

View File

@@ -51,6 +51,7 @@
"dependencies": {
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-slot": "^1.0.1",
"@radix-ui/react-tabs": "^1.0.4",
"@scm-manager/ui-buttons": "2.46.2-SNAPSHOT"
}
}

View File

@@ -42,6 +42,10 @@ import {
DataPageHeaderSettingLabel,
DataPageHeaderSettings,
} from "./templates/data-page/DataPageHeader";
import TabsComponent from "./tabs/Tabs";
import TabsContent from "./tabs/TabsContent";
import TabsList from "./tabs/TabsList";
import TabTrigger from "./tabs/TabTrigger";
export { default as Collapsible } from "./collapsible/Collapsible";
@@ -80,3 +84,10 @@ export const DataPageHeader = Object.assign(DataPageHeaderComponent, {
}),
CreateButton: DataPageHeaderCreateButton,
});
export const Tabs = Object.assign(TabsComponent, {
List: Object.assign(TabsList, {
Trigger: TabTrigger,
}),
Content: TabsContent,
});

View File

@@ -0,0 +1,46 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { ComponentProps } from "react";
import classNames from "classnames";
import * as HeadlessTabs from "@radix-ui/react-tabs";
type Props = ComponentProps<typeof HeadlessTabs.Trigger>;
/**
* @beta
* @since 2.47.0
*/
const TabTrigger = React.forwardRef<HTMLButtonElement, Props>(({ children, className, ...props }, ref) => (
<li>
<HeadlessTabs.Trigger
className={classNames("has-background-transparent is-borderless", className)}
ref={ref}
{...props}
>
{children}
</HeadlessTabs.Trigger>
</li>
));
export default TabTrigger;

View File

@@ -0,0 +1,48 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from "react";
import StoryRouter from "storybook-react-router";
import { ComponentMeta, ComponentStory } from "@storybook/react";
import Tabs from "./Tabs";
import TabsList from "./TabsList";
import TabTrigger from "./TabTrigger";
import TabsContent from "./TabsContent";
export default {
title: "Tab",
component: Tabs,
decorators: [StoryRouter()],
} as ComponentMeta<typeof Tabs>;
export const Default: ComponentStory<typeof Tabs> = () => (
<Tabs defaultValue="tab2">
<TabsList>
<TabTrigger value="tab1">Account</TabTrigger>
<TabTrigger value="tab2">Password</TabTrigger>
</TabsList>
<TabsContent value="tab1">Account Settings</TabsContent>
<TabsContent value="tab2">Password Settings</TabsContent>
</Tabs>
);

View File

@@ -0,0 +1,52 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { ComponentProps, useCallback, useState } from "react";
import * as HeadlessTabs from "@radix-ui/react-tabs";
type Props = Omit<ComponentProps<typeof HeadlessTabs.Root>, "value">;
/**
* @beta
* @since 2.47.0
*/
const Tabs = React.forwardRef<HTMLDivElement, Props>(({ defaultValue, onValueChange, children, ...props }, ref) => {
const [value, setValue] = useState(defaultValue);
const handleValueChange = useCallback(
(newValue: string) => {
setValue(newValue);
if (onValueChange) {
onValueChange(newValue);
}
},
[onValueChange]
);
return (
<HeadlessTabs.Root {...props} ref={ref} onValueChange={handleValueChange} value={value}>
{children}
</HeadlessTabs.Root>
);
});
export default Tabs;

View File

@@ -0,0 +1,33 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import * as RadixTabs from "@radix-ui/react-tabs";
/**
* @beta
* @since 2.47.0
*/
const TabsContent = RadixTabs.Content;
export default TabsContent;

View File

@@ -0,0 +1,41 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { HTMLAttributes } from "react";
import * as RadixTabs from "@radix-ui/react-tabs";
import classNames from "classnames";
/**
* @beta
* @since 2.47.0
*/
const TabsList = React.forwardRef<HTMLUListElement, HTMLAttributes<HTMLUListElement>>(({ children, ...props }, ref) => (
<RadixTabs.List className={classNames("tabs", /* required for focus-outline */ "p-1")}>
<ul {...props} ref={ref}>
{children}
</ul>
</RadixTabs.List>
));
export default TabsList;

View File

@@ -10,7 +10,7 @@
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.11.2",
"bulma": "^0.9.3",
"bulma": "https://github.com/scm-manager/bulma#bbe5bdb66fce67e2513ab8caefb1eac5504e1402",
"bulma-popover": "^1.0.0",
"react-diff-view": "^2.4.10"
},
@@ -31,4 +31,4 @@
"eslintConfig": {
"extends": "@scm-manager/eslint-config"
}
}
}

View File

@@ -0,0 +1,97 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC, useState } from "react";
import CompareSelectorListEntry from "./CompareSelectorListEntry";
import DefaultBranchTag from "../branches/components/DefaultBranchTag";
import { Branch, Repository } from "@scm-manager/ui-types";
import { CompareFunction, CompareProps, CompareTypes } from "./CompareSelectBar";
import { useBranches } from "@scm-manager/ui-api";
import { ErrorNotification, Loading, Notification } from "@scm-manager/ui-components";
import styled from "styled-components";
import { useTranslation } from "react-i18next";
type Props = {
onSelect: CompareFunction;
selected: CompareProps;
repository: Repository;
filter: string;
};
const FixedWidthNotification = styled(Notification)`
width: 18.5rem;
margin-top: 0.5rem;
`;
const ScrollableUl = styled.ul`
max-height: 15.65rem;
width: 18.5rem;
overflow-x: hidden;
overflow-y: auto;
`;
const BranchTab: FC<Props> = ({ onSelect, selected, repository, filter }) => {
const [t] = useTranslation("repos");
const { isLoading: branchesIsLoading, error: branchesError, data: branchesData } = useBranches(repository);
const branches: Branch[] = (branchesData?._embedded?.branches as Branch[]) || [];
const [selection, setSelection] = useState(selected);
const onSelectEntry = (type: CompareTypes, name: string) => {
setSelection({ type, name });
onSelect(type, name);
};
if (branchesIsLoading) {
return <Loading />;
}
if (branchesError) {
return <ErrorNotification error={branchesError} />;
}
const elements = branches.filter((branch) => branch.name.includes(filter));
if (elements.length === 0) {
return <FixedWidthNotification>{t("compare.selector.emptyResult")}</FixedWidthNotification>;
}
return (
<ScrollableUl className="py-2 pr-2" role="listbox">
{elements.map((branch) => {
return (
<CompareSelectorListEntry
isSelected={selection.type === "b" && selection.name === branch.name}
onClick={() => onSelectEntry("b", branch.name)}
key={branch.name}
>
<span className="is-ellipsis-overflow">{branch.name}</span>
<DefaultBranchTag className="ml-2" defaultBranch={branch.defaultBranch} />
</CompareSelectorListEntry>
);
})}
</ScrollableUl>
);
};
export default BranchTab;

View File

@@ -52,7 +52,10 @@ const CompareRoot: FC<Props> = ({ repository, baseUrl }) => {
<CompareView repository={repository} baseUrl={baseUrl} />
</Route>
{data._embedded && (
<Redirect from={url} to={`${url}/b/${data._embedded.branches.filter(b => b.defaultBranch)[0].name}`} />
<Redirect
from={url}
to={`${url}/b/${encodeURIComponent(data._embedded.branches.filter((b) => b.defaultBranch)[0].name)}`}
/>
)}
</Switch>
);

View File

@@ -27,9 +27,13 @@ import { useTranslation } from "react-i18next";
import classNames from "classnames";
import styled from "styled-components";
import { Repository } from "@scm-manager/ui-types";
import { devices, Icon } from "@scm-manager/ui-components";
import CompareSelectorList from "./CompareSelectorList";
import { devices } from "@scm-manager/ui-components";
import { Tabs } from "@scm-manager/ui-layout";
import { Icon } from "@scm-manager/ui-buttons";
import { CompareFunction, CompareProps, CompareTypes } from "./CompareSelectBar";
import BranchTab from "./BranchTab";
import TagTab from "./TagTab";
import RevisionTab from "./RevisionTab";
type Props = {
onSelect: CompareFunction;
@@ -114,7 +118,7 @@ const CompareSelector: FC<Props> = ({ onSelect, selected, label, repository }) =
<strong>{getActionTypeName(selection.type)}:</strong> {selection.name}
</span>
<span className="icon is-small">
<Icon name="angle-down" color="inherit" />
<Icon>angle-down</Icon>
</span>
</button>
</MaxWidthDiv>
@@ -128,15 +132,25 @@ const CompareSelector: FC<Props> = ({ onSelect, selected, label, repository }) =
<input
className="input is-small"
placeholder={t("compare.selector.filter")}
onChange={e => setFilter(e.target.value)}
onChange={(e) => setFilter(e.target.value)}
type="search"
/>
<CompareSelectorList
onSelect={onSelectEntry}
selected={selected}
repository={repository}
filter={filter}
/>
<Tabs className="is-small mt-3 mb-0" defaultValue="branch">
<Tabs.List aria-label={t("compare.selector.title")}>
<Tabs.List.Trigger value="branch">{t("compare.selector.tabs.b")}</Tabs.List.Trigger>
<Tabs.List.Trigger value="tag">{t("compare.selector.tabs.t")}</Tabs.List.Trigger>
<Tabs.List.Trigger value="revision">{t("compare.selector.tabs.r")}</Tabs.List.Trigger>
</Tabs.List>
<Tabs.Content value="branch">
<BranchTab onSelect={onSelectEntry} selected={selected} repository={repository} filter={filter} />
</Tabs.Content>
<Tabs.Content value="tag">
<TagTab onSelect={onSelectEntry} selected={selected} repository={repository} filter={filter} />
</Tabs.Content>
<Tabs.Content value="revision">
<RevisionTab onSelect={onSelectEntry} selected={selected} />
</Tabs.Content>
</Tabs>
</div>
</BorderedMenu>
</div>

View File

@@ -1,272 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC, KeyboardEvent, useState } from "react";
import { useTranslation } from "react-i18next";
import classNames from "classnames";
import styled from "styled-components";
import { Branch, Repository, Tag } from "@scm-manager/ui-types";
import { useBranches, useTags } from "@scm-manager/ui-api";
import { Button, ErrorNotification, Loading, NoStyleButton, Notification } from "@scm-manager/ui-components";
import DefaultBranchTag from "../branches/components/DefaultBranchTag";
import CompareSelectorListEntry from "./CompareSelectorListEntry";
import { CompareFunction, CompareProps, CompareTypes } from "./CompareSelectBar";
type Props = {
onSelect: CompareFunction;
selected: CompareProps;
repository: Repository;
filter: string;
};
const TabStyleButton = styled(NoStyleButton)`
align-items: center;
border-bottom: var(--scm-border);
color: var(--scm-secondary-text);
display: flex;
justify-content: center;
margin-bottom: -1px;
padding: 0.5rem 1rem;
vertical-align: top;
&:hover {
border-bottom-color: var(--scm-hover-color);
color: var(--scm-hover-color);
}
&.is-active {
border-bottom-color: var(--scm-info-color);
color: var(--scm-info-color);
}
&:focus-visible {
background-color: var(--scm-column-selection);
}
`;
const ScrollableUl = styled.ul`
max-height: 15.65rem;
width: 18.5rem;
overflow-x: hidden;
overflow-y: scroll;
`;
const SizedDiv = styled.div`
width: 18.5rem;
`;
const SmallButton = styled(Button)`
height: 1.875rem;
`;
type BranchTabContentProps = {
elements: Branch[];
selection: CompareProps;
onSelectEntry: CompareFunction;
};
const EmptyResultNotification: FC = () => {
const [t] = useTranslation("repos");
return <Notification type="info">{t("compare.selector.emptyResult")}</Notification>;
};
const BranchTabContent: FC<BranchTabContentProps> = ({ elements, selection, onSelectEntry }) => {
if (elements.length === 0) {
return <EmptyResultNotification />;
}
return (
<>
{elements.map(branch => {
return (
<CompareSelectorListEntry
isSelected={selection.type === "b" && selection.name === branch.name}
onClick={() => onSelectEntry("b", branch.name)}
key={branch.name}
>
<span className="is-ellipsis-overflow">{branch.name}</span>
<DefaultBranchTag className="ml-2" defaultBranch={branch.defaultBranch} />
</CompareSelectorListEntry>
);
})}
</>
);
};
type TagTabContentProps = {
elements: Tag[];
selection: CompareProps;
onSelectEntry: CompareFunction;
};
const TagTabContent: FC<TagTabContentProps> = ({ elements, selection, onSelectEntry }) => {
if (elements.length === 0) {
return <EmptyResultNotification />;
}
return (
<>
{elements.map(tag => (
<CompareSelectorListEntry
isSelected={selection.type === "t" && selection.name === tag.name}
onClick={() => onSelectEntry("t", tag.name)}
key={tag.name}
>
<span className="is-ellipsis-overflow">{tag.name}</span>
</CompareSelectorListEntry>
))}
</>
);
};
type RevisionTabContentProps = {
selected: CompareProps;
onSelect: CompareFunction;
};
const RevisionTabContent: FC<RevisionTabContentProps> = ({ selected, onSelect }) => {
const [t] = useTranslation("repos");
const defaultValue = selected.type === "r" ? selected.name : "";
const [revision, setRevision] = useState(defaultValue);
const handleKeyPress = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") {
event.preventDefault();
handleSubmit();
}
};
const handleSubmit = () => {
if (revision) {
onSelect("r", revision);
}
};
return (
<SizedDiv className="mt-2">
<div className="field has-addons is-justify-content-center">
<div className="control">
<input
className="input is-small"
placeholder={t("compare.selector.revision.input")}
onChange={e => setRevision(e.target.value.trim())}
onKeyPress={handleKeyPress}
value={revision.trim()}
/>
</div>
<div className="control">
<SmallButton className="is-info is-small" action={handleSubmit} disabled={!revision}>
{t("compare.selector.revision.submit")}
</SmallButton>
</div>
</div>
</SizedDiv>
);
};
const ScrollableList: FC<{ selectedTab: CompareTypes } & Props> = ({
selectedTab,
onSelect,
selected,
repository,
filter
}) => {
const { isLoading: branchesIsLoading, error: branchesError, data: branchesData } = useBranches(repository);
const branches: Branch[] = (branchesData?._embedded?.branches as Branch[]) || [];
const { isLoading: tagsIsLoading, error: tagsError, data: tagsData } = useTags(repository);
const tags: Tag[] = (tagsData?._embedded?.tags as Tag[]) || [];
const [selection, setSelection] = useState(selected);
const onSelectEntry = (type: CompareTypes, name: string) => {
setSelection({ type, name });
onSelect(type, name);
};
if (branchesIsLoading || tagsIsLoading) {
return <Loading />;
}
if (branchesError || tagsError) {
return <ErrorNotification error={branchesError || tagsError} />;
}
if (selectedTab !== "r") {
return (
<ScrollableUl className="py-2 pr-2" aria-expanded="true" role="listbox">
{selectedTab === "b" && (
<BranchTabContent
elements={branches.filter(branch => branch.name.includes(filter))}
selection={selection}
onSelectEntry={onSelectEntry}
/>
)}
{selectedTab === "t" && (
<TagTabContent
elements={tags.filter(tag => tag.name.includes(filter))}
selection={selection}
onSelectEntry={onSelectEntry}
/>
)}
</ScrollableUl>
);
}
return null;
};
const CompareSelectorList: FC<Props> = ({ onSelect, selected, repository, filter }) => {
const [t] = useTranslation("repos");
const [selectedTab, setSelectedTab] = useState<CompareTypes>(selected.type);
const tabs: CompareTypes[] = ["b", "t", "r"];
return (
<>
<div className="tabs is-small mt-3 mb-0">
<ul>
{tabs.map(tab => {
return (
<li key={tab}>
<TabStyleButton
className={classNames({ "is-active": selectedTab === tab })}
onClick={() => setSelectedTab(tab)}
>
{t("compare.selector.tabs." + tab)}
</TabStyleButton>
</li>
);
})}
</ul>
</div>
<ScrollableList
selectedTab={selectedTab}
onSelect={onSelect}
selected={selected}
repository={repository}
filter={filter}
/>
{selectedTab === "r" && <RevisionTabContent onSelect={onSelect} selected={selected} />}
</>
);
};
export default CompareSelectorList;

View File

@@ -0,0 +1,80 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { CompareFunction, CompareProps } from "./CompareSelectBar";
import React, { FC } from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { Button } from "@scm-manager/ui-buttons";
import { Input } from "@scm-manager/ui-forms";
import { SubmitHandler, useForm } from "react-hook-form";
type Props = {
selected: CompareProps;
onSelect: CompareFunction;
};
type FormProps = {
revision: string;
};
const SizedForm = styled.form`
width: 18.5rem;
`;
const SmallButton = styled(Button)`
height: 1.875rem;
`;
const RevisionTab: FC<Props> = ({ selected, onSelect }) => {
const [t] = useTranslation("repos");
const defaultValue = selected.type === "r" ? selected.name : "";
const { register, handleSubmit, watch } = useForm<FormProps>({
defaultValues: {
revision: defaultValue,
},
});
const onSubmit: SubmitHandler<FormProps> = (data) => onSelect("r", data.revision);
return (
<SizedForm className="mt-2" onSubmit={handleSubmit(onSubmit)}>
<div className="field has-addons is-justify-content-center">
<div className="control">
<Input
className="is-small"
placeholder={t("compare.selector.revision.input")}
{...register("revision", { required: true })}
/>
</div>
<div className="control">
<SmallButton className="is-info is-small" type="submit" disabled={!watch("revision")}>
{t("compare.selector.revision.submit")}
</SmallButton>
</div>
</div>
</SizedForm>
);
};
export default RevisionTab;

View File

@@ -0,0 +1,93 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC, useState } from "react";
import CompareSelectorListEntry from "./CompareSelectorListEntry";
import { Repository, Tag } from "@scm-manager/ui-types";
import { CompareFunction, CompareProps, CompareTypes } from "./CompareSelectBar";
import { useTags } from "@scm-manager/ui-api";
import { ErrorNotification, Loading, Notification } from "@scm-manager/ui-components";
import styled from "styled-components";
import { useTranslation } from "react-i18next";
type Props = {
onSelect: CompareFunction;
selected: CompareProps;
repository: Repository;
filter: string;
};
const FixedWidthNotification = styled(Notification)`
width: 18.5rem;
margin-top: 0.5rem;
`;
const ScrollableUl = styled.ul`
max-height: 15.65rem;
width: 18.5rem;
overflow-x: hidden;
overflow-y: auto;
`;
const TagTab: FC<Props> = ({ onSelect, selected, repository, filter }) => {
const [t] = useTranslation("repos");
const { isLoading: tagsIsLoading, error: tagsError, data: tagsData } = useTags(repository);
const tags: Tag[] = (tagsData?._embedded?.tags as Tag[]) || [];
const [selection, setSelection] = useState(selected);
const onSelectEntry = (type: CompareTypes, name: string) => {
setSelection({ type, name });
onSelect(type, name);
};
if (tagsIsLoading) {
return <Loading />;
}
if (tagsError) {
return <ErrorNotification error={tagsError} />;
}
const elements = tags.filter((tag) => tag.name.includes(filter));
if (elements.length === 0) {
return <FixedWidthNotification>{t("compare.selector.emptyResult")}</FixedWidthNotification>;
}
return (
<ScrollableUl className="py-2 pr-2" role="listbox">
{elements.map((tag) => (
<CompareSelectorListEntry
isSelected={selection.type === "t" && selection.name === tag.name}
onClick={() => onSelectEntry("t", tag.name)}
key={tag.name}
>
<span className="is-ellipsis-overflow">{tag.name}</span>
</CompareSelectorListEntry>
))}
</ScrollableUl>
);
};
export default TagTab;

View File

@@ -2929,6 +2929,21 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-tabs@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz#993608eec55a5d1deddd446fa9978d2bc1053da2"
integrity sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.1"
"@radix-ui/react-context" "1.0.1"
"@radix-ui/react-direction" "1.0.1"
"@radix-ui/react-id" "1.0.1"
"@radix-ui/react-presence" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-roving-focus" "1.0.4"
"@radix-ui/react-use-controllable-state" "1.0.1"
"@radix-ui/react-tooltip@1.0.2":
version "1.0.2"
resolved "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.2.tgz"
@@ -8107,10 +8122,9 @@ bulma-popover@^1.0.0:
resolved "https://registry.npmjs.org/bulma-popover/-/bulma-popover-1.1.1.tgz"
integrity sha512-QvYJq04usgiCvTwnEZr/xtStkKRTToO/tc7JVcdtRBIBBWNaUu4tfHAOFXDMav8NFIvN8JOiQEtHO1dHNvVr1Q==
bulma@^0.9.3:
"bulma@https://github.com/scm-manager/bulma#bbe5bdb66fce67e2513ab8caefb1eac5504e1402":
version "0.9.4"
resolved "https://registry.npmjs.org/bulma/-/bulma-0.9.4.tgz"
integrity sha512-86FlT5+1GrsgKbPLRRY7cGDg8fsJiP/jzTqXXVqiUZZ2aZT8uemEOHlU1CDU+TxklPEZ11HZNNWclRBBecP4CQ==
resolved "https://github.com/scm-manager/bulma#bbe5bdb66fce67e2513ab8caefb1eac5504e1402"
bundle-require@^3.0.2:
version "3.0.4"
@@ -18132,7 +18146,7 @@ react-transition-group@^2.2.1:
react@17, react@17.0.2, react@^16.8.3, react@^17.0.1:
version "17.0.2"
resolved "https://registry.npmjs.org/react/-/react-17.0.2.tgz"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
dependencies:
loose-envify "^1.1.0"