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
|
subtitle: Tags
|
||||||
---
|
---
|
||||||
### Übersicht
|
### Ü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
|
subtitle: Tags
|
||||||
---
|
---
|
||||||
### Overview
|
### 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>;
|
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
|
* @beta
|
||||||
* @since 2.41.0
|
* @since 2.41.0
|
||||||
@@ -82,7 +84,9 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
type LinkButtonProps = BaseButtonProps & ReactRouterLinkProps;
|
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
|
* @beta
|
||||||
* @since 2.41.0
|
* @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.
|
* External links open in a new browser tab with rel flags "noopener" and "noreferrer" set by default.
|
||||||
*
|
*
|
||||||
* @beta
|
* @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
|
* @since 2.41.0
|
||||||
|
* @see ExternalLink
|
||||||
*/
|
*/
|
||||||
export const ExternalLinkButton = React.forwardRef<HTMLAnchorElement, ExternalLinkButtonProps>(
|
export const ExternalLinkButton = React.forwardRef<HTMLAnchorElement, ExternalLinkButtonProps>(
|
||||||
({ className, variant, isLoading, testId, children, ...props }, ref) => (
|
({ className, variant, isLoading, testId, children, ...props }, ref) => (
|
||||||
<a
|
<ExternalLink
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer noopener"
|
|
||||||
{...props}
|
{...props}
|
||||||
className={classNames(createButtonClasses(variant, isLoading), className)}
|
className={classNames(createButtonClasses(variant, isLoading), className)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...createAttributesForTesting(testId)}
|
{...createAttributesForTesting(testId)}
|
||||||
>
|
>
|
||||||
{children}
|
{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
|
* @beta
|
||||||
* @since 2.44.0
|
* @since 2.44.0
|
||||||
* @see https://bulma.io/documentation/elements/icon/
|
* @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) => {
|
const Icon = React.forwardRef<HTMLElement, Props>(({ children, className, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<span className={classNames(className, "icon")} {...props} ref={ref}>
|
<span className={classNames(className, "icon")} aria-hidden="true" {...props} ref={ref}>
|
||||||
<i
|
<i
|
||||||
className={classNames(`fas fa-fw fa-${children}`, {
|
className={classNames(`fas fa-fw fa-${children}`, {
|
||||||
"fa-xs": className?.includes("is-small"),
|
"fa-xs": className?.includes("is-small"),
|
||||||
|
|||||||
@@ -22,5 +22,5 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { Button, LinkButton, ExternalLinkButton, ButtonVariants } from "./Button";
|
export { Button, LinkButton, ExternalLinkButton, ExternalLink, ButtonVariants } from "./Button";
|
||||||
export { default as Icon } from "./Icon";
|
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-dom": "17",
|
||||||
"react-router-dom": "5",
|
"react-router-dom": "5",
|
||||||
"classnames": "2",
|
"classnames": "2",
|
||||||
"styled-components": "5"
|
"styled-components": "5",
|
||||||
|
"react-i18next": "11"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-tooltip": "1.0.2",
|
"@radix-ui/react-tooltip": "1.0.2",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import MenuComponent, { MenuButton, MenuExternalLink, MenuLink } from "./menu/Menu";
|
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";
|
export { default as Tooltip } from "./tooltip/Tooltip";
|
||||||
|
|
||||||
@@ -32,5 +32,5 @@ export const Menu = Object.assign(MenuComponent, {
|
|||||||
Link: MenuLink,
|
Link: MenuLink,
|
||||||
ExternalLink: MenuExternalLink,
|
ExternalLink: MenuExternalLink,
|
||||||
Trigger: MenuTrigger,
|
Trigger: MenuTrigger,
|
||||||
DEFAULT_TRIGGER: DEFAULT_MENU_TRIGGER,
|
DefaultTrigger: DefaultMenuTrigger,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
import React, { AnchorHTMLAttributes, ButtonHTMLAttributes, FC } from "react";
|
import React, { AnchorHTMLAttributes, ButtonHTMLAttributes, FC } from "react";
|
||||||
import * as RadixMenu from "@radix-ui/react-dropdown-menu";
|
import * as RadixMenu from "@radix-ui/react-dropdown-menu";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { DEFAULT_MENU_TRIGGER } from "./MenuTrigger";
|
import { DefaultMenuTrigger } from "./MenuTrigger";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Link as ReactRouterLink, LinkProps as ReactRouterLinkProps } from "react-router-dom";
|
import { Link as ReactRouterLink, LinkProps as ReactRouterLinkProps } from "react-router-dom";
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ type Props = {
|
|||||||
* @since 2.44.0
|
* @since 2.44.0
|
||||||
* @see https://www.w3.org/WAI/ARIA/apg/patterns/menubar/
|
* @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 (
|
return (
|
||||||
<RadixMenu.Root>
|
<RadixMenu.Root>
|
||||||
{trigger}
|
{trigger}
|
||||||
|
|||||||
@@ -25,7 +25,8 @@
|
|||||||
import React, { ComponentProps } from "react";
|
import React, { ComponentProps } from "react";
|
||||||
import { Button, Icon } from "@scm-manager/ui-buttons";
|
import { Button, Icon } from "@scm-manager/ui-buttons";
|
||||||
import * as RadixMenu from "@radix-ui/react-dropdown-menu";
|
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>;
|
type Props = ComponentProps<typeof Button>;
|
||||||
|
|
||||||
@@ -41,19 +42,22 @@ const MenuTrigger = React.forwardRef<HTMLButtonElement, Props>(({ children, ...p
|
|||||||
</RadixMenu.Trigger>
|
</RadixMenu.Trigger>
|
||||||
));
|
));
|
||||||
|
|
||||||
const StyledMenuTrigger = styled(MenuTrigger)`
|
|
||||||
padding-left: 1em;
|
|
||||||
padding-right: 1em;
|
|
||||||
`;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @beta
|
* @beta
|
||||||
* @since 2.44.0
|
* @since 2.44.0
|
||||||
*/
|
*/
|
||||||
export const DEFAULT_MENU_TRIGGER = (
|
export const DefaultMenuTrigger = React.forwardRef<HTMLButtonElement, Props>(({ className, ...props }, ref) => {
|
||||||
<StyledMenuTrigger className="is-borderless has-background-transparent has-hover-color-blue">
|
const [t] = useTranslation("commons");
|
||||||
<Icon>ellipsis-v</Icon>
|
return (
|
||||||
</StyledMenuTrigger>
|
<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>
|
||||||
|
</MenuTrigger>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export default MenuTrigger;
|
export default MenuTrigger;
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
--scm-success-color: #{$success};
|
--scm-success-color: #{$success};
|
||||||
--scm-warning-color: #{$warning};
|
--scm-warning-color: #{$warning};
|
||||||
--scm-danger-color: #{$danger};
|
--scm-danger-color: #{$danger};
|
||||||
|
--scm-hover-color-blue: #{scale-color($blue, $alpha: -90%)};
|
||||||
|
|
||||||
--scm-secondary-least-color: #{$secondary-least};
|
--scm-secondary-least-color: #{$secondary-least};
|
||||||
--scm-secondary-less-color: #{$secondary-less};
|
--scm-secondary-less-color: #{$secondary-less};
|
||||||
@@ -367,6 +368,7 @@ button, .button {
|
|||||||
padding-left: 1.5em;
|
padding-left: 1.5em;
|
||||||
padding-right: 1.5em;
|
padding-right: 1.5em;
|
||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
|
min-width: 2.5rem;
|
||||||
font-weight: $weight-semibold;
|
font-weight: $weight-semibold;
|
||||||
|
|
||||||
&.is-primary,
|
&.is-primary,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"@scm-manager/ui-forms": "2.43.1-SNAPSHOT",
|
"@scm-manager/ui-forms": "2.43.1-SNAPSHOT",
|
||||||
"@scm-manager/ui-buttons": "2.43.1-SNAPSHOT",
|
"@scm-manager/ui-buttons": "2.43.1-SNAPSHOT",
|
||||||
"@scm-manager/ui-overlays": "2.43.1-SNAPSHOT",
|
"@scm-manager/ui-overlays": "2.43.1-SNAPSHOT",
|
||||||
|
"@scm-manager/ui-layout": "2.43.1-SNAPSHOT",
|
||||||
"classnames": "^2.2.5",
|
"classnames": "^2.2.5",
|
||||||
"history": "^4.10.1",
|
"history": "^4.10.1",
|
||||||
"i18next": "21",
|
"i18next": "21",
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"menu": {
|
||||||
|
"defaultTriggerLabel": "Menü"
|
||||||
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"submit": "Speichern",
|
"submit": "Speichern",
|
||||||
"reset": "Leeren",
|
"reset": "Leeren",
|
||||||
|
|||||||
@@ -191,9 +191,17 @@
|
|||||||
},
|
},
|
||||||
"tags": {
|
"tags": {
|
||||||
"overview": {
|
"overview": {
|
||||||
"title": "Übersicht aller verfügbaren Tags",
|
"title": "Tags",
|
||||||
"noTags": "Keine Tags gefunden.",
|
"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": {
|
"table": {
|
||||||
"tags": "Tags"
|
"tags": "Tags"
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"menu": {
|
||||||
|
"defaultTriggerLabel": "Menu"
|
||||||
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
"reset": "Clear",
|
"reset": "Clear",
|
||||||
|
|||||||
@@ -191,9 +191,17 @@
|
|||||||
},
|
},
|
||||||
"tags": {
|
"tags": {
|
||||||
"overview": {
|
"overview": {
|
||||||
"title": "Overview of All Tags",
|
"title": "Tags",
|
||||||
"noTags": "No tags found.",
|
"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": {
|
"table": {
|
||||||
"tags": "Tags"
|
"tags": "Tags"
|
||||||
|
|||||||
@@ -25,10 +25,13 @@ import React, { FC } from "react";
|
|||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Tag, Link } from "@scm-manager/ui-types";
|
import { Link, Tag } from "@scm-manager/ui-types";
|
||||||
import { Button, DateFromNow } from "@scm-manager/ui-components";
|
import { DateFromNow } from "@scm-manager/ui-components";
|
||||||
import { useKeyboardIteratorTarget } from "@scm-manager/ui-shortcuts";
|
import { useKeyboardIteratorTarget } from "@scm-manager/ui-shortcuts";
|
||||||
import { encodePart } from "../../sources/components/content/FileLink";
|
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 = {
|
type Props = {
|
||||||
tag: Tag;
|
tag: Tag;
|
||||||
@@ -41,26 +44,32 @@ const TagRow: FC<Props> = ({ tag, baseUrl, onDelete }) => {
|
|||||||
const [t] = useTranslation("repos");
|
const [t] = useTranslation("repos");
|
||||||
const ref = useKeyboardIteratorTarget();
|
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`;
|
const to = `${baseUrl}/${encodePart(tag.name)}/info`;
|
||||||
return (
|
return (
|
||||||
<tr>
|
<CardList.Card
|
||||||
<td className="is-vertical-align-middle">
|
key={tag.name}
|
||||||
<RouterLink ref={ref} to={to} title={tag.name}>
|
action={
|
||||||
{tag.name}
|
(tag?._links?.delete as Link)?.href ? (
|
||||||
<span className={classNames("has-text-secondary", "is-ellipsis-overflow", "ml-2", "is-size-7")}>
|
<Menu>
|
||||||
{t("tags.overview.created")} <DateFromNow date={tag.date} />
|
<Menu.Button onSelect={() => onDelete(tag)}>
|
||||||
</span>
|
<Icon>trash</Icon>
|
||||||
</RouterLink>
|
{t("tag.delete.button")}
|
||||||
</td>
|
</Menu.Button>
|
||||||
<td className="is-vertical-align-middle has-text-centered">{deleteButton}</td>
|
</Menu>
|
||||||
</tr>
|
) : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CardList.Card.Row>
|
||||||
|
<CardList.Card.Title>
|
||||||
|
<RouterLink ref={ref} to={to}>
|
||||||
|
{tag.name}
|
||||||
|
</RouterLink>
|
||||||
|
</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 TagRow from "./TagRow";
|
||||||
import { ConfirmAlert, ErrorNotification } from "@scm-manager/ui-components";
|
import { ConfirmAlert, ErrorNotification } from "@scm-manager/ui-components";
|
||||||
import { useDeleteTag } from "@scm-manager/ui-api";
|
import { useDeleteTag } from "@scm-manager/ui-api";
|
||||||
|
import { CardListBox } from "@scm-manager/ui-layout";
|
||||||
import { KeyboardIterator } from "@scm-manager/ui-shortcuts";
|
import { KeyboardIterator } from "@scm-manager/ui-shortcuts";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -90,20 +91,13 @@ const TagTable: FC<Props> = ({ repository, baseUrl, tags }) => {
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{error ? <ErrorNotification error={error} /> : null}
|
{error ? <ErrorNotification error={error} /> : null}
|
||||||
<table className="card-table table is-hoverable is-fullwidth is-word-break">
|
<CardListBox>
|
||||||
<thead>
|
<KeyboardIterator>
|
||||||
<tr>
|
{tags.map((tag) => (
|
||||||
<th>{t("tags.table.tags")}</th>
|
<TagRow key={tag.name} baseUrl={baseUrl} tag={tag} onDelete={onDelete} />
|
||||||
</tr>
|
))}
|
||||||
</thead>
|
</KeyboardIterator>
|
||||||
<tbody>
|
</CardListBox>
|
||||||
<KeyboardIterator>
|
|
||||||
{tags.map((tag) => (
|
|
||||||
<TagRow key={tag.name} baseUrl={baseUrl} tag={tag} onDelete={onDelete} />
|
|
||||||
))}
|
|
||||||
</KeyboardIterator>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,13 +22,14 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from "react";
|
import React, { FC, useMemo, useState } from "react";
|
||||||
import { Repository } from "@scm-manager/ui-types";
|
import { Repository } from "@scm-manager/ui-types";
|
||||||
import { ErrorNotification, Loading, Notification, Subtitle } from "@scm-manager/ui-components";
|
import { ErrorNotification, Loading, Notification, Subtitle } from "@scm-manager/ui-components";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import orderTags from "../orderTags";
|
import orderTags, { SORT_OPTIONS, SortOption } from "../orderTags";
|
||||||
import TagTable from "../components/TagTable";
|
import TagTable from "../components/TagTable";
|
||||||
import { useTags } from "@scm-manager/ui-api";
|
import { useTags } from "@scm-manager/ui-api";
|
||||||
|
import { Select } from "@scm-manager/ui-forms";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
@@ -38,6 +39,8 @@ type Props = {
|
|||||||
const TagsOverview: FC<Props> = ({ repository, baseUrl }) => {
|
const TagsOverview: FC<Props> = ({ repository, baseUrl }) => {
|
||||||
const { isLoading, error, data } = useTags(repository);
|
const { isLoading, error, data } = useTags(repository);
|
||||||
const [t] = useTranslation("repos");
|
const [t] = useTranslation("repos");
|
||||||
|
const [sort, setSort] = useState<SortOption | undefined>();
|
||||||
|
const tags = useMemo(() => orderTags(data?._embedded?.tags || [], sort), [data, sort]);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <ErrorNotification error={error} />;
|
return <ErrorNotification error={error} />;
|
||||||
@@ -47,12 +50,19 @@ const TagsOverview: FC<Props> = ({ repository, baseUrl }) => {
|
|||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags = data?._embedded?.tags || [];
|
|
||||||
orderTags(tags);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Subtitle subtitle={t("tags.overview.title")} />
|
<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 ? (
|
{tags.length > 0 ? (
|
||||||
<TagTable repository={repository} baseUrl={baseUrl} tags={tags} />
|
<TagTable repository={repository} baseUrl={baseUrl} tags={tags} />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -49,4 +49,14 @@ describe("order tags", () => {
|
|||||||
orderTags(tags);
|
orderTags(tags);
|
||||||
expect(tags).toEqual([tag2, tag3, tag1]);
|
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
|
// sort tags by date beginning with latest first
|
||||||
import { Tag } from "@scm-manager/ui-types";
|
import { Tag } from "@scm-manager/ui-types";
|
||||||
|
|
||||||
export default (tags: Tag[]) => {
|
export const SORT_OPTIONS = ["default", "name_asc", "name_desc"] as const;
|
||||||
tags.sort((a, b) => {
|
|
||||||
return new Date(b.date) - new Date(a.date);
|
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