mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-05-06 19:46:05 +02:00
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:
4
gradle/changelog/accessibility_tabpanel.yaml
Normal file
4
gradle/changelog/accessibility_tabpanel.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
- type: fixed
|
||||
description: Make compare accessible
|
||||
- type: fixed
|
||||
description: Compare target when default branch contains slash
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
46
scm-ui/ui-layout/src/tabs/TabTrigger.tsx
Normal file
46
scm-ui/ui-layout/src/tabs/TabTrigger.tsx
Normal 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;
|
||||
48
scm-ui/ui-layout/src/tabs/Tabs.stories.tsx
Normal file
48
scm-ui/ui-layout/src/tabs/Tabs.stories.tsx
Normal 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>
|
||||
);
|
||||
52
scm-ui/ui-layout/src/tabs/Tabs.tsx
Normal file
52
scm-ui/ui-layout/src/tabs/Tabs.tsx
Normal 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;
|
||||
33
scm-ui/ui-layout/src/tabs/TabsContent.tsx
Normal file
33
scm-ui/ui-layout/src/tabs/TabsContent.tsx
Normal 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;
|
||||
41
scm-ui/ui-layout/src/tabs/TabsList.tsx
Normal file
41
scm-ui/ui-layout/src/tabs/TabsList.tsx
Normal 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;
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
97
scm-ui/ui-webapp/src/repos/compare/BranchTab.tsx
Normal file
97
scm-ui/ui-webapp/src/repos/compare/BranchTab.tsx
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
80
scm-ui/ui-webapp/src/repos/compare/RevisionTab.tsx
Normal file
80
scm-ui/ui-webapp/src/repos/compare/RevisionTab.tsx
Normal 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;
|
||||
93
scm-ui/ui-webapp/src/repos/compare/TagTab.tsx
Normal file
93
scm-ui/ui-webapp/src/repos/compare/TagTab.tsx
Normal 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;
|
||||
22
yarn.lock
22
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user