mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-12 16:35:45 +01:00
Refactor repository tags overview
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 96 KiB |
@@ -3,7 +3,9 @@ title: Repository
|
||||
subtitle: Tags
|
||||
---
|
||||
### Übersicht
|
||||
Auf der Tags-Übersicht sind die existierenden Tags nach Erstelldatum absteigend aufgeführt. Bei einem Klick auf einen Tag wird der Benutzer zur Detailseite des Tags weitergeleitet.
|
||||
Auf der Tags-Übersicht sind die existierenden Tags aufgeführt.
|
||||
Die Tags sind standardmäßig nach Erstelldatum absteigend sortiert, können aber auch alphabetisch sortiert werden.
|
||||
Bei einem Klick auf einen Tag wird der Benutzer zur Detailseite des Tags weitergeleitet.
|
||||
|
||||

|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 95 KiB |
@@ -3,7 +3,9 @@ title: Repository
|
||||
subtitle: Tags
|
||||
---
|
||||
### Overview
|
||||
The tag overview shows the tags that exist for this repository. By clicking on a tag, the details page of the tag is shown.
|
||||
The tag overview shows the tags that exist for this repository.
|
||||
The tags are sorted by creation date in descending order by default, but can also be sorted alphabetically.
|
||||
By clicking on a tag, the details page of the tag is shown.
|
||||
|
||||

|
||||
|
||||
|
||||
4
gradle/changelog/card_list_component.yaml
Normal file
4
gradle/changelog/card_list_component.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
- type: added
|
||||
description: New card list component
|
||||
- type: changed
|
||||
description: Revamp repository tags overview
|
||||
@@ -60,7 +60,9 @@ type BaseButtonProps = {
|
||||
type ButtonProps = BaseButtonProps & ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
/**
|
||||
* Styled html button
|
||||
* Styled html button.
|
||||
*
|
||||
* A button has to declare an `aria-label` if it exclusively contains an {@link Icon} as its children.
|
||||
*
|
||||
* @beta
|
||||
* @since 2.41.0
|
||||
@@ -82,7 +84,9 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
type LinkButtonProps = BaseButtonProps & ReactRouterLinkProps;
|
||||
|
||||
/**
|
||||
* Styled react router link
|
||||
* Styled react router link.
|
||||
*
|
||||
* A button has to declare an `aria-label` if it exclusively contains an {@link Icon} as its children.
|
||||
*
|
||||
* @beta
|
||||
* @since 2.41.0
|
||||
@@ -100,27 +104,40 @@ export const LinkButton = React.forwardRef<HTMLAnchorElement, LinkButtonProps>(
|
||||
)
|
||||
);
|
||||
|
||||
type ExternalLinkButtonProps = BaseButtonProps & AnchorHTMLAttributes<HTMLAnchorElement>;
|
||||
|
||||
/**
|
||||
* Styled html anchor.
|
||||
*
|
||||
* External links open in a new browser tab with rel flags "noopener" and "noreferrer" set by default.
|
||||
*
|
||||
* @beta
|
||||
* @since 2.44.0
|
||||
*/
|
||||
export const ExternalLink = React.forwardRef<HTMLAnchorElement, AnchorHTMLAttributes<HTMLAnchorElement>>(
|
||||
({ children, ...props }, ref) => (
|
||||
<a target="_blank" rel="noreferrer noopener" {...props} ref={ref}>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
);
|
||||
|
||||
type ExternalLinkButtonProps = BaseButtonProps & AnchorHTMLAttributes<HTMLAnchorElement>;
|
||||
|
||||
/**
|
||||
* Styled {@link ExternalLink}.
|
||||
*
|
||||
* A button has to declare an `aria-label` if it exclusively contains an {@link Icon} as its children.
|
||||
*
|
||||
* @beta
|
||||
* @since 2.41.0
|
||||
* @see ExternalLink
|
||||
*/
|
||||
export const ExternalLinkButton = React.forwardRef<HTMLAnchorElement, ExternalLinkButtonProps>(
|
||||
({ className, variant, isLoading, testId, children, ...props }, ref) => (
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
<ExternalLink
|
||||
{...props}
|
||||
className={classNames(createButtonClasses(variant, isLoading), className)}
|
||||
ref={ref}
|
||||
{...createAttributesForTesting(testId)}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
</ExternalLink>
|
||||
)
|
||||
);
|
||||
|
||||
@@ -30,13 +30,20 @@ type Props = React.HTMLProps<HTMLElement> & {
|
||||
};
|
||||
|
||||
/**
|
||||
* Icons are hidden to assistive technologies by default.
|
||||
*
|
||||
* If your icon does convey a state, unset `aria-hidden` and set an appropriate `aria-label`.
|
||||
*
|
||||
* The children have to be a single text node containing a valid fontawesome icon name.
|
||||
*
|
||||
* @beta
|
||||
* @since 2.44.0
|
||||
* @see https://bulma.io/documentation/elements/icon/
|
||||
* @see https://fontawesome.com/search?o=r&m=free
|
||||
*/
|
||||
const Icon = React.forwardRef<HTMLElement, Props>(({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<span className={classNames(className, "icon")} {...props} ref={ref}>
|
||||
<span className={classNames(className, "icon")} aria-hidden="true" {...props} ref={ref}>
|
||||
<i
|
||||
className={classNames(`fas fa-fw fa-${children}`, {
|
||||
"fa-xs": className?.includes("is-small"),
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
export { Button, LinkButton, ExternalLinkButton, ButtonVariants } from "./Button";
|
||||
export { Button, LinkButton, ExternalLinkButton, ExternalLink, ButtonVariants } from "./Button";
|
||||
export { default as Icon } from "./Icon";
|
||||
|
||||
4
scm-ui/ui-layout/.storybook/.babelrc
Normal file
4
scm-ui/ui-layout/.storybook/.babelrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"presets": ["@scm-manager/babel-preset"],
|
||||
"plugins": ["@babel/plugin-syntax-dynamic-import"]
|
||||
}
|
||||
57
scm-ui/ui-layout/.storybook/RemoveThemesPlugin.js
Normal file
57
scm-ui/ui-layout/.storybook/RemoveThemesPlugin.js
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
|
||||
class RemoveThemesPlugin {
|
||||
apply (compiler) {
|
||||
compiler.hooks.compilation.tap('RemoveThemesPlugin', (compilation) => {
|
||||
|
||||
HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration.tapAsync(
|
||||
'RemoveThemesPlugin',
|
||||
(data, cb) => {
|
||||
|
||||
// remove generated style-loader bundles from the page
|
||||
// there should be a better way, which does not generate the bundles at all
|
||||
// but for now it works
|
||||
if (data.assets.js) {
|
||||
data.assets.js = data.assets.js.filter(bundle => !bundle.startsWith("ui-theme-"))
|
||||
.filter(bundle => !bundle.startsWith("runtime~ui-theme-"))
|
||||
}
|
||||
|
||||
// remove css links to avoid conflicts with the themes
|
||||
// so we remove all and add our own via preview-head.html
|
||||
if (data.assets.css) {
|
||||
data.assets.css = data.assets.css.filter(css => !css.startsWith("ui-theme-"))
|
||||
}
|
||||
|
||||
// Tell webpack to move on
|
||||
cb(null, data)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RemoveThemesPlugin
|
||||
89
scm-ui/ui-layout/.storybook/main.js
Normal file
89
scm-ui/ui-layout/.storybook/main.js
Normal file
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
const RemoveThemesPlugin = require("./RemoveThemesPlugin");
|
||||
|
||||
const root = path.resolve("..");
|
||||
|
||||
const themedir = path.join(root, "ui-styles", "src");
|
||||
|
||||
const themes = fs
|
||||
.readdirSync(themedir)
|
||||
.map((filename) => path.parse(filename))
|
||||
.filter((p) => p.ext === ".scss")
|
||||
.reduce((entries, current) => ({ ...entries, [`ui-theme-${current.name}`]: path.join(themedir, current.base) }), {});
|
||||
|
||||
module.exports = {
|
||||
typescript: { reactDocgen: false },
|
||||
core: {
|
||||
builder: "webpack5",
|
||||
},
|
||||
stories: ["../src/**/*.stories.@(js|jsx|ts|tsx|mdx)"],
|
||||
addons: [
|
||||
"storybook-addon-i18next",
|
||||
"storybook-addon-themes",
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
"@storybook/addon-interactions",
|
||||
"@storybook/addon-a11y",
|
||||
"storybook-addon-pseudo-states",
|
||||
"storybook-addon-mock",
|
||||
],
|
||||
framework: "@storybook/react",
|
||||
webpackFinal: async (config) => {
|
||||
// add our themes to webpack entry points
|
||||
config.entry = {
|
||||
main: config.entry,
|
||||
...themes,
|
||||
};
|
||||
|
||||
// create separate css files for our themes
|
||||
config.plugins.push(
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "[name].css",
|
||||
ignoreOrder: false,
|
||||
})
|
||||
);
|
||||
|
||||
config.module.rules.push({
|
||||
test: /\.scss$/,
|
||||
use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
|
||||
});
|
||||
|
||||
// the html-webpack-plugin adds the generated css and js files to the iframe,
|
||||
// which overrides our manually loaded css files.
|
||||
// So we use a custom plugin which uses a hook of html-webpack-plugin
|
||||
// to filter our themes from the output.
|
||||
config.plugins.push(new RemoveThemesPlugin());
|
||||
|
||||
// force cjs instead of esm
|
||||
// https://github.com/tannerlinsley/react-query/issues/3513
|
||||
config.resolve.alias["react-query/devtools"] = require.resolve("react-query/devtools");
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
26
scm-ui/ui-layout/.storybook/preview-head.html
Normal file
26
scm-ui/ui-layout/.storybook/preview-head.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
<link id="ui-theme" data-theme="light" rel="stylesheet" type="text/css" href="/ui-theme-light.css">
|
||||
|
||||
67
scm-ui/ui-layout/.storybook/preview.js
Normal file
67
scm-ui/ui-layout/.storybook/preview.js
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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, { useEffect } from "react";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import i18n from "i18next";
|
||||
import { withThemes } from "storybook-addon-themes";
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
whitelist: ["en", "de"],
|
||||
lng: "en",
|
||||
fallbackLng: "en",
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
react: {
|
||||
useSuspense: false,
|
||||
},
|
||||
});
|
||||
|
||||
const Decorator = ({ children, themeName }) => {
|
||||
useEffect(() => {
|
||||
const link = document.querySelector("#ui-theme");
|
||||
if (link && link["data-theme"] !== themeName) {
|
||||
link.href = `ui-theme-${themeName}.css`;
|
||||
link["data-theme"] = themeName;
|
||||
}
|
||||
}, [themeName]);
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export const decorators = [withThemes];
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
themes: {
|
||||
Decorator,
|
||||
clearable: false,
|
||||
default: "light",
|
||||
list: [
|
||||
{ name: "light", color: "#fff" },
|
||||
{ name: "highcontrast", color: "#050514" },
|
||||
{ name: "dark", color: "#121212" },
|
||||
],
|
||||
},
|
||||
};
|
||||
49
scm-ui/ui-layout/package.json
Normal file
49
scm-ui/ui-layout/package.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@scm-manager/ui-layout",
|
||||
"private": true,
|
||||
"version": "2.43.1-SNAPSHOT",
|
||||
"main": "build/index.js",
|
||||
"types": "build/index.d.ts",
|
||||
"module": "build/index.mjs",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "tsup ./src/index.ts -d build --format esm,cjs --dts",
|
||||
"storybook": "start-storybook -p 6006",
|
||||
"build-storybook": "build-storybook"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.19.0",
|
||||
"@scm-manager/eslint-config": "^2.16.0",
|
||||
"@scm-manager/prettier-config": "^2.10.1",
|
||||
"@scm-manager/tsconfig": "^2.13.0",
|
||||
"@scm-manager/ui-styles": "2.43.1-SNAPSHOT",
|
||||
"@scm-manager/ui-overlays": "2.43.1-SNAPSHOT",
|
||||
"@scm-manager/ui-buttons": "2.43.1-SNAPSHOT",
|
||||
"@storybook/addon-actions": "^6.5.10",
|
||||
"@storybook/addon-essentials": "^6.5.10",
|
||||
"@storybook/addon-interactions": "^6.5.10",
|
||||
"@storybook/addon-links": "^6.5.10",
|
||||
"@storybook/builder-webpack5": "^6.5.10",
|
||||
"@storybook/manager-webpack5": "^6.5.10",
|
||||
"@storybook/react": "^6.5.10",
|
||||
"@storybook/testing-library": "^0.0.13",
|
||||
"@storybook/addon-docs": "^6.5.14",
|
||||
"babel-loader": "^8.2.5",
|
||||
"storybook-addon-mock": "^3.2.0",
|
||||
"storybook-addon-themes": "^6.1.0",
|
||||
"tsup": "^6.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "17",
|
||||
"react-dom": "17",
|
||||
"classnames": "2",
|
||||
"styled-components": "5"
|
||||
},
|
||||
"prettier": "@scm-manager/prettier-config",
|
||||
"eslintConfig": {
|
||||
"extends": "@scm-manager/eslint-config"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
}
|
||||
}
|
||||
57
scm-ui/ui-layout/src/card-list/Card.tsx
Normal file
57
scm-ui/ui-layout/src/card-list/Card.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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, { LiHTMLAttributes } from "react";
|
||||
import styled from "styled-components";
|
||||
import classNames from "classnames";
|
||||
|
||||
const CardElement = styled.li`
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) min-content;
|
||||
grid-template-rows: auto;
|
||||
`;
|
||||
|
||||
const CardActionContainer = styled.span`
|
||||
grid-column: -1;
|
||||
grid-row: 1;
|
||||
margin-top: -0.5rem;
|
||||
margin-right: -0.5rem;
|
||||
`;
|
||||
|
||||
type Props = LiHTMLAttributes<HTMLLIElement> & {
|
||||
action?: React.ReactElement;
|
||||
};
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.44.0
|
||||
*/
|
||||
const Card = React.forwardRef<HTMLLIElement, Props>(({ className, children, action, ...props }, ref) => (
|
||||
<CardElement className={classNames(className, "is-relative", "p-2")} ref={ref} {...props}>
|
||||
{children}
|
||||
{action ? <CardActionContainer>{action}</CardActionContainer> : null}
|
||||
</CardElement>
|
||||
));
|
||||
|
||||
export default Card;
|
||||
117
scm-ui/ui-layout/src/card-list/CardList.stories.tsx
Normal file
117
scm-ui/ui-layout/src/card-list/CardList.stories.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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 StoryRouter from "storybook-react-router";
|
||||
import { ComponentMeta, StoryFn } from "@storybook/react";
|
||||
import React, { ComponentProps } from "react";
|
||||
import { ExtractProps } from "@scm-manager/ui-extensions";
|
||||
import { Link } from "react-router-dom";
|
||||
import CardList, { CardListBox } from "./CardList";
|
||||
import Card from "./Card";
|
||||
import CardTitle from "./CardTitle";
|
||||
import { Menu } from "@scm-manager/ui-overlays";
|
||||
import { Icon } from "@scm-manager/ui-buttons";
|
||||
import CardRow from "./CardRow";
|
||||
|
||||
export default {
|
||||
title: "CardList",
|
||||
component: CardList,
|
||||
decorators: [StoryRouter()],
|
||||
} as ComponentMeta<typeof CardList>;
|
||||
|
||||
const Template: StoryFn<ExtractProps<typeof CardListBox>> = (args) => <CardListBox {...args} />;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
||||
Default.args = {
|
||||
children: [
|
||||
// eslint-disable-next-line no-console
|
||||
<Card>
|
||||
<CardRow>
|
||||
<CardTitle>My favorite repository</CardTitle>
|
||||
</CardRow>
|
||||
</Card>,
|
||||
<Card>
|
||||
<CardRow>
|
||||
<CardTitle>
|
||||
<Link aria-label="Edit My least liked repo" to="/cards/1">
|
||||
My least liked repo
|
||||
</Link>
|
||||
</CardTitle>
|
||||
</CardRow>
|
||||
</Card>,
|
||||
<Card
|
||||
action={
|
||||
<Menu>
|
||||
<Menu.Button>
|
||||
<Icon>trash</Icon>
|
||||
Delete
|
||||
</Menu.Button>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<CardRow className="is-flex is-justify-content-space-between">
|
||||
<CardTitle>
|
||||
<Link aria-label="Edit My other favorite repository" to="/cards/1">
|
||||
My other favorite repository
|
||||
</Link>
|
||||
</CardTitle>
|
||||
(TAG)
|
||||
</CardRow>
|
||||
<CardRow className="is-size-6">
|
||||
This is a card description in the second row. Highlighting how the layout flows if there are multiple rows in
|
||||
one card while the card also has an action.
|
||||
</CardRow>
|
||||
<CardRow className="is-size-6 is-flex is-justify-content-space-between">
|
||||
<span>This is a third row, lets see how this works out.</span>(MERGED)
|
||||
</CardRow>
|
||||
</Card>,
|
||||
<Card>
|
||||
<CardRow className="is-flex is-align-items-center">
|
||||
<CardTitle>
|
||||
<Link
|
||||
aria-label="Edit Enhance descriptions to differentiate between dumps with and without metadata."
|
||||
to="/cards/1"
|
||||
>
|
||||
Enhance descriptions to differentiate between dumps with and without metadata.
|
||||
</Link>
|
||||
</CardTitle>
|
||||
<small>#13456</small>
|
||||
</CardRow>
|
||||
<CardRow className="is-size-6">
|
||||
Another Name requested to merge <strong>feature/asdkjertg</strong> into <strong>develop</strong> about 3 months
|
||||
ago.
|
||||
</CardRow>
|
||||
<CardRow className="is-size-6 is-flex is-justify-content-space-between">
|
||||
<div>
|
||||
<span>Tasks (3/3)</span>
|
||||
<span>Reviewers (1)</span>
|
||||
<span>Analyses (✓)</span>
|
||||
<span>Workflow (✓)</span>
|
||||
</div>
|
||||
<span>(OPEN)</span>
|
||||
</CardRow>
|
||||
</Card>,
|
||||
],
|
||||
} as ComponentProps<typeof CardListBox>;
|
||||
67
scm-ui/ui-layout/src/card-list/CardList.tsx
Normal file
67
scm-ui/ui-layout/src/card-list/CardList.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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 classNames from "classnames";
|
||||
import styled from "styled-components";
|
||||
|
||||
const CardListElement = styled.ul`
|
||||
> * + * {
|
||||
border-top: var(--scm-border);
|
||||
margin-top: 0.5rem;
|
||||
padding-top: 1rem !important;
|
||||
|
||||
*:is(h1, h2, h3, h4, h5, h6) a::after {
|
||||
top: 0.5rem !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = HTMLAttributes<HTMLUListElement>;
|
||||
|
||||
/**
|
||||
* The {@link CardList.Card.Title} is currently represented as a `h3`, which means the list can only be used on the top level of the page without breaking accessibility.
|
||||
*
|
||||
* @beta
|
||||
* @since 2.44.0
|
||||
*/
|
||||
const CardList = React.forwardRef<HTMLUListElement, Props>(({ children, className, ...props }, ref) => (
|
||||
<CardListElement ref={ref} {...props} className={classNames(className, "is-flex", "is-flex-direction-column")}>
|
||||
{children}
|
||||
</CardListElement>
|
||||
));
|
||||
|
||||
/**
|
||||
* The {@link CardList.Card.Title} is currently represented as a `h3`, which means the list can only be used on the top level of the page without breaking accessibility.
|
||||
*
|
||||
* @beta
|
||||
* @since 2.44.0
|
||||
*/
|
||||
export const CardListBox = React.forwardRef<HTMLUListElement, Props>(({ className, children, ...props }, ref) => (
|
||||
<CardList className={classNames(className, "p-2 box")} ref={ref} {...props}>
|
||||
{children}
|
||||
</CardList>
|
||||
));
|
||||
|
||||
export default CardList;
|
||||
45
scm-ui/ui-layout/src/card-list/CardRow.tsx
Normal file
45
scm-ui/ui-layout/src/card-list/CardRow.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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 styled from "styled-components";
|
||||
import classNames from "classnames";
|
||||
|
||||
const CardRowElement = styled.div`
|
||||
grid-column: 1 / 2;
|
||||
`;
|
||||
|
||||
type Props = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.44.0
|
||||
*/
|
||||
const CardRow = React.forwardRef<HTMLDivElement, Props>(({ className, children, ...props }, ref) => (
|
||||
<CardRowElement className={classNames(className)} ref={ref} {...props}>
|
||||
{children}
|
||||
</CardRowElement>
|
||||
));
|
||||
|
||||
export default CardRow;
|
||||
79
scm-ui/ui-layout/src/card-list/CardTitle.tsx
Normal file
79
scm-ui/ui-layout/src/card-list/CardTitle.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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 styled from "styled-components";
|
||||
import classNames from "classnames";
|
||||
|
||||
const CardTitleElement = styled.h3`
|
||||
a {
|
||||
color: var(--scm-secondary-text);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
&::after {
|
||||
outline: #af3ee7 3px solid;
|
||||
outline-offset: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
background-color: var(--scm-hover-color-blue);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = HTMLAttributes<HTMLHeadingElement>;
|
||||
|
||||
/**
|
||||
* A card title may contain a link as its only child which will be automatically stretched to cover the whole card area.
|
||||
*
|
||||
* If a card title has a link, individual card elements which should be interactive have to get the `is-relative` class.
|
||||
*
|
||||
* The card title (or enclosed link) content must be an accessible text and must not contain any other interactive elements.
|
||||
*
|
||||
* You can wrap the title in a {@link CardList.Card.Row} to introduce other elements next to the title.
|
||||
*
|
||||
* The title (or its enclosing row) must be the first element in a {@link CardList.Card}.
|
||||
*
|
||||
* @beta
|
||||
* @since 2.44.0
|
||||
*/
|
||||
const CardTitle = React.forwardRef<HTMLHeadingElement, Props>(({ children, className, ...props }, ref) => (
|
||||
<CardTitleElement className={classNames(className, "is-ellipsis-overflow")} ref={ref} {...props}>
|
||||
{children}
|
||||
</CardTitleElement>
|
||||
));
|
||||
|
||||
export default CardTitle;
|
||||
38
scm-ui/ui-layout/src/index.ts
Normal file
38
scm-ui/ui-layout/src/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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 CardListComponent, { CardListBox as CardListBoxComponent } from "./card-list/CardList";
|
||||
import CardRow from "./card-list/CardRow";
|
||||
import Card from "./card-list/Card";
|
||||
import CardTitle from "./card-list/CardTitle";
|
||||
|
||||
const CardListExport = {
|
||||
Card: Object.assign(Card, {
|
||||
Row: CardRow,
|
||||
Title: CardTitle,
|
||||
}),
|
||||
};
|
||||
|
||||
export const CardList = Object.assign(CardListComponent, CardListExport);
|
||||
export const CardListBox = Object.assign(CardListBoxComponent, CardListExport);
|
||||
3
scm-ui/ui-layout/tsconfig.json
Normal file
3
scm-ui/ui-layout/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "@scm-manager/tsconfig"
|
||||
}
|
||||
@@ -36,7 +36,8 @@
|
||||
"react-dom": "17",
|
||||
"react-router-dom": "5",
|
||||
"classnames": "2",
|
||||
"styled-components": "5"
|
||||
"styled-components": "5",
|
||||
"react-i18next": "11"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-tooltip": "1.0.2",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
*/
|
||||
|
||||
import MenuComponent, { MenuButton, MenuExternalLink, MenuLink } from "./menu/Menu";
|
||||
import MenuTrigger, { DEFAULT_MENU_TRIGGER } from "./menu/MenuTrigger";
|
||||
import MenuTrigger, { DefaultMenuTrigger } from "./menu/MenuTrigger";
|
||||
|
||||
export { default as Tooltip } from "./tooltip/Tooltip";
|
||||
|
||||
@@ -32,5 +32,5 @@ export const Menu = Object.assign(MenuComponent, {
|
||||
Link: MenuLink,
|
||||
ExternalLink: MenuExternalLink,
|
||||
Trigger: MenuTrigger,
|
||||
DEFAULT_TRIGGER: DEFAULT_MENU_TRIGGER,
|
||||
DefaultTrigger: DefaultMenuTrigger,
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
import React, { AnchorHTMLAttributes, ButtonHTMLAttributes, FC } from "react";
|
||||
import * as RadixMenu from "@radix-ui/react-dropdown-menu";
|
||||
import styled from "styled-components";
|
||||
import { DEFAULT_MENU_TRIGGER } from "./MenuTrigger";
|
||||
import { DefaultMenuTrigger } from "./MenuTrigger";
|
||||
import classNames from "classnames";
|
||||
import { Link as ReactRouterLink, LinkProps as ReactRouterLinkProps } from "react-router-dom";
|
||||
|
||||
@@ -116,7 +116,7 @@ type Props = {
|
||||
* @since 2.44.0
|
||||
* @see https://www.w3.org/WAI/ARIA/apg/patterns/menubar/
|
||||
*/
|
||||
const Menu: FC<Props> = ({ children, side, className, trigger = DEFAULT_MENU_TRIGGER }) => {
|
||||
const Menu: FC<Props> = ({ children, side, className, trigger = <DefaultMenuTrigger /> }) => {
|
||||
return (
|
||||
<RadixMenu.Root>
|
||||
{trigger}
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
import React, { ComponentProps } from "react";
|
||||
import { Button, Icon } from "@scm-manager/ui-buttons";
|
||||
import * as RadixMenu from "@radix-ui/react-dropdown-menu";
|
||||
import styled from "styled-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import classNames from "classnames";
|
||||
|
||||
type Props = ComponentProps<typeof Button>;
|
||||
|
||||
@@ -41,19 +42,22 @@ const MenuTrigger = React.forwardRef<HTMLButtonElement, Props>(({ children, ...p
|
||||
</RadixMenu.Trigger>
|
||||
));
|
||||
|
||||
const StyledMenuTrigger = styled(MenuTrigger)`
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
`;
|
||||
|
||||
/**
|
||||
* @beta
|
||||
* @since 2.44.0
|
||||
*/
|
||||
export const DEFAULT_MENU_TRIGGER = (
|
||||
<StyledMenuTrigger className="is-borderless has-background-transparent has-hover-color-blue">
|
||||
export const DefaultMenuTrigger = React.forwardRef<HTMLButtonElement, Props>(({ className, ...props }, ref) => {
|
||||
const [t] = useTranslation("commons");
|
||||
return (
|
||||
<MenuTrigger
|
||||
aria-label={t("menu.defaultTriggerLabel")}
|
||||
className={classNames(className, "is-borderless has-background-transparent has-hover-color-blue px-2")}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<Icon>ellipsis-v</Icon>
|
||||
</StyledMenuTrigger>
|
||||
</MenuTrigger>
|
||||
);
|
||||
});
|
||||
|
||||
export default MenuTrigger;
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
--scm-success-color: #{$success};
|
||||
--scm-warning-color: #{$warning};
|
||||
--scm-danger-color: #{$danger};
|
||||
--scm-hover-color-blue: #{scale-color($blue, $alpha: -90%)};
|
||||
|
||||
--scm-secondary-least-color: #{$secondary-least};
|
||||
--scm-secondary-less-color: #{$secondary-less};
|
||||
@@ -367,6 +368,7 @@ button, .button {
|
||||
padding-left: 1.5em;
|
||||
padding-right: 1.5em;
|
||||
height: 2.5rem;
|
||||
min-width: 2.5rem;
|
||||
font-weight: $weight-semibold;
|
||||
|
||||
&.is-primary,
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"@scm-manager/ui-forms": "2.43.1-SNAPSHOT",
|
||||
"@scm-manager/ui-buttons": "2.43.1-SNAPSHOT",
|
||||
"@scm-manager/ui-overlays": "2.43.1-SNAPSHOT",
|
||||
"@scm-manager/ui-layout": "2.43.1-SNAPSHOT",
|
||||
"classnames": "^2.2.5",
|
||||
"history": "^4.10.1",
|
||||
"i18next": "21",
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"menu": {
|
||||
"defaultTriggerLabel": "Menü"
|
||||
},
|
||||
"form": {
|
||||
"submit": "Speichern",
|
||||
"reset": "Leeren",
|
||||
|
||||
@@ -191,9 +191,17 @@
|
||||
},
|
||||
"tags": {
|
||||
"overview": {
|
||||
"title": "Übersicht aller verfügbaren Tags",
|
||||
"title": "Tags",
|
||||
"noTags": "Keine Tags gefunden.",
|
||||
"created": "Erstellt"
|
||||
"created": "Erstellt",
|
||||
"sort": {
|
||||
"label": "Sortierung nach",
|
||||
"option": {
|
||||
"default": "Als letztes erstellt",
|
||||
"name_asc": "Name A-Z",
|
||||
"name_desc": "Name Z-A"
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"tags": "Tags"
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"menu": {
|
||||
"defaultTriggerLabel": "Menu"
|
||||
},
|
||||
"form": {
|
||||
"submit": "Submit",
|
||||
"reset": "Clear",
|
||||
|
||||
@@ -191,9 +191,17 @@
|
||||
},
|
||||
"tags": {
|
||||
"overview": {
|
||||
"title": "Overview of All Tags",
|
||||
"title": "Tags",
|
||||
"noTags": "No tags found.",
|
||||
"created": "Created"
|
||||
"created": "Created",
|
||||
"sort": {
|
||||
"label": "Sort by",
|
||||
"option": {
|
||||
"default": "Most recently created",
|
||||
"name_asc": "Name A-Z",
|
||||
"name_desc": "Name Z-A"
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"tags": "Tags"
|
||||
|
||||
@@ -25,10 +25,13 @@ import React, { FC } from "react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import classNames from "classnames";
|
||||
import { Tag, Link } from "@scm-manager/ui-types";
|
||||
import { Button, DateFromNow } from "@scm-manager/ui-components";
|
||||
import { Link, Tag } from "@scm-manager/ui-types";
|
||||
import { DateFromNow } from "@scm-manager/ui-components";
|
||||
import { useKeyboardIteratorTarget } from "@scm-manager/ui-shortcuts";
|
||||
import { encodePart } from "../../sources/components/content/FileLink";
|
||||
import { Menu } from "@scm-manager/ui-overlays";
|
||||
import { CardList } from "@scm-manager/ui-layout";
|
||||
import { Icon } from "@scm-manager/ui-buttons";
|
||||
|
||||
type Props = {
|
||||
tag: Tag;
|
||||
@@ -41,26 +44,32 @@ const TagRow: FC<Props> = ({ tag, baseUrl, onDelete }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
const ref = useKeyboardIteratorTarget();
|
||||
|
||||
let deleteButton;
|
||||
if ((tag?._links?.delete as Link)?.href) {
|
||||
deleteButton = (
|
||||
<Button color="text" icon="trash" action={() => onDelete(tag)} title={t("tag.delete.button")} className="px-2" />
|
||||
);
|
||||
}
|
||||
|
||||
const to = `${baseUrl}/${encodePart(tag.name)}/info`;
|
||||
return (
|
||||
<tr>
|
||||
<td className="is-vertical-align-middle">
|
||||
<RouterLink ref={ref} to={to} title={tag.name}>
|
||||
<CardList.Card
|
||||
key={tag.name}
|
||||
action={
|
||||
(tag?._links?.delete as Link)?.href ? (
|
||||
<Menu>
|
||||
<Menu.Button onSelect={() => onDelete(tag)}>
|
||||
<Icon>trash</Icon>
|
||||
{t("tag.delete.button")}
|
||||
</Menu.Button>
|
||||
</Menu>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<CardList.Card.Row>
|
||||
<CardList.Card.Title>
|
||||
<RouterLink ref={ref} to={to}>
|
||||
{tag.name}
|
||||
<span className={classNames("has-text-secondary", "is-ellipsis-overflow", "ml-2", "is-size-7")}>
|
||||
{t("tags.overview.created")} <DateFromNow date={tag.date} />
|
||||
</span>
|
||||
</RouterLink>
|
||||
</td>
|
||||
<td className="is-vertical-align-middle has-text-centered">{deleteButton}</td>
|
||||
</tr>
|
||||
</CardList.Card.Title>
|
||||
</CardList.Card.Row>
|
||||
<CardList.Card.Row className={classNames("is-size-7", "has-text-secondary")}>
|
||||
{t("tags.overview.created")} <DateFromNow className="is-relative" date={tag.date} />
|
||||
</CardList.Card.Row>
|
||||
</CardList.Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import { useTranslation } from "react-i18next";
|
||||
import TagRow from "./TagRow";
|
||||
import { ConfirmAlert, ErrorNotification } from "@scm-manager/ui-components";
|
||||
import { useDeleteTag } from "@scm-manager/ui-api";
|
||||
import { CardListBox } from "@scm-manager/ui-layout";
|
||||
import { KeyboardIterator } from "@scm-manager/ui-shortcuts";
|
||||
|
||||
type Props = {
|
||||
@@ -90,20 +91,13 @@ const TagTable: FC<Props> = ({ repository, baseUrl, tags }) => {
|
||||
/>
|
||||
) : null}
|
||||
{error ? <ErrorNotification error={error} /> : null}
|
||||
<table className="card-table table is-hoverable is-fullwidth is-word-break">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("tags.table.tags")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<CardListBox>
|
||||
<KeyboardIterator>
|
||||
{tags.map((tag) => (
|
||||
<TagRow key={tag.name} baseUrl={baseUrl} tag={tag} onDelete={onDelete} />
|
||||
))}
|
||||
</KeyboardIterator>
|
||||
</tbody>
|
||||
</table>
|
||||
</CardListBox>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -22,13 +22,14 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React, { FC } from "react";
|
||||
import React, { FC, useMemo, useState } from "react";
|
||||
import { Repository } from "@scm-manager/ui-types";
|
||||
import { ErrorNotification, Loading, Notification, Subtitle } from "@scm-manager/ui-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import orderTags from "../orderTags";
|
||||
import orderTags, { SORT_OPTIONS, SortOption } from "../orderTags";
|
||||
import TagTable from "../components/TagTable";
|
||||
import { useTags } from "@scm-manager/ui-api";
|
||||
import { Select } from "@scm-manager/ui-forms";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
@@ -38,6 +39,8 @@ type Props = {
|
||||
const TagsOverview: FC<Props> = ({ repository, baseUrl }) => {
|
||||
const { isLoading, error, data } = useTags(repository);
|
||||
const [t] = useTranslation("repos");
|
||||
const [sort, setSort] = useState<SortOption | undefined>();
|
||||
const tags = useMemo(() => orderTags(data?._embedded?.tags || [], sort), [data, sort]);
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
@@ -47,12 +50,19 @@ const TagsOverview: FC<Props> = ({ repository, baseUrl }) => {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const tags = data?._embedded?.tags || [];
|
||||
orderTags(tags);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Subtitle subtitle={t("tags.overview.title")} />
|
||||
<div className="is-flex is-align-items-center mb-3">
|
||||
<label className="mr-2" htmlFor="tags-overview-sort">
|
||||
{t("tags.overview.sort.label")}
|
||||
</label>
|
||||
<Select id="tags-overview-sort" onChange={(e) => setSort(e.target.value as SortOption)}>
|
||||
{SORT_OPTIONS.map((sortOption) => (
|
||||
<option value={sortOption}>{t(`tags.overview.sort.option.${sortOption}`)}</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
{tags.length > 0 ? (
|
||||
<TagTable repository={repository} baseUrl={baseUrl} tags={tags} />
|
||||
) : (
|
||||
|
||||
@@ -49,4 +49,14 @@ describe("order tags", () => {
|
||||
orderTags(tags);
|
||||
expect(tags).toEqual([tag2, tag3, tag1]);
|
||||
});
|
||||
it("should order tags ascending by name", () => {
|
||||
const tags = [tag1, tag2, tag3];
|
||||
orderTags(tags, "name_asc");
|
||||
expect(tags).toEqual([tag1, tag2, tag3]);
|
||||
});
|
||||
it("should order tags descending by name", () => {
|
||||
const tags = [tag1, tag2, tag3];
|
||||
orderTags(tags, "name_desc");
|
||||
expect(tags).toEqual([tag3, tag2, tag1]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,8 +25,20 @@
|
||||
// sort tags by date beginning with latest first
|
||||
import { Tag } from "@scm-manager/ui-types";
|
||||
|
||||
export default (tags: Tag[]) => {
|
||||
tags.sort((a, b) => {
|
||||
export const SORT_OPTIONS = ["default", "name_asc", "name_desc"] as const;
|
||||
|
||||
export type SortOption = typeof SORT_OPTIONS[number];
|
||||
|
||||
export default (tags: Tag[], sort?: SortOption) => {
|
||||
return tags.sort((a, b) => {
|
||||
switch (sort) {
|
||||
case "name_asc":
|
||||
return a.name > b.name ? 1 : -1;
|
||||
case "name_desc":
|
||||
return a.name > b.name ? -1 : 1;
|
||||
default:
|
||||
// @ts-ignore Comparing dates is a valid operation. It is unknown why typescript shows an error here.
|
||||
return new Date(b.date) - new Date(a.date);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user