added first version of annotate ui component

This commit is contained in:
Sebastian Sdorra
2020-06-12 08:19:33 +02:00
parent 4cb898edbb
commit 9a66efc693
8 changed files with 331 additions and 22 deletions

View File

@@ -0,0 +1,112 @@
/*
* 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 { storiesOf } from "@storybook/react";
import * as React from "react";
import styled from "styled-components";
import Annotate, { AnnotatedSource } from "./Annotate";
const Wrapper = styled.div`
margin: 2rem;
`;
const commitCreateNewApp = {
revision: "0d8c1d328f4599b363755671afe667c7ace52bae",
author: {
name: "Arthur Dent",
mail: "arthur.dent@hitchhiker.com"
},
description: "create new app",
when: new Date()
};
const commitFixedMissingImport = {
revision: "fab38559ce3ab8c388e067712b4bd7ab94b9fa9b",
author: {
name: "Tricia Marie McMillan",
mail: "trillian@hitchhiker.com"
},
description: "fixed missing import",
when: new Date()
};
const commitImplementMain = {
revision: "5203292ab2bc0c020dd22adc4d3897da4930e43f",
author: {
name: "Ford Prefect",
mail: "ford.prefect@hitchhiker.com"
},
description: "implemented main function",
when: new Date()
};
const source: AnnotatedSource = {
language: "go",
lines: [
{
lineNumber: 1,
code: "package main",
...commitCreateNewApp
},
{
lineNumber: 2,
code: "",
...commitCreateNewApp
},
{
lineNumber: 3,
code: 'import "fmt"',
...commitFixedMissingImport
},
{
lineNumber: 4,
code: "",
...commitFixedMissingImport
},
{
lineNumber: 5,
code: "func main() {",
...commitCreateNewApp
},
{
lineNumber: 6,
code: ' fmt.Println("Hello World")',
...commitImplementMain
},
{
lineNumber: 7,
code: "}",
...commitCreateNewApp
},
{
lineNumber: 8,
code: "",
...commitCreateNewApp
}
]
};
storiesOf("Annotate", module)
.addDecorator(storyFn => <Wrapper className="box box-link-shadow">{storyFn()}</Wrapper>)
.add("Default", () => <Annotate source={source} />);

View File

@@ -0,0 +1,135 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC } from "react";
import { Person } from "@scm-manager/ui-types";
// @ts-ignore
import { LightAsync as ReactSyntaxHighlighter, createElement } from "react-syntax-highlighter";
// @ts-ignore
import { arduinoLight } from "react-syntax-highlighter/dist/cjs/styles/hljs";
import styled from "styled-components";
import DateShort from "./DateShort";
// TODO move types to ui-types
export type AnnotatedSource = {
lines: AnnotatedLine[];
language: string;
};
export type AnnotatedLine = {
author: Person;
code: string;
description: string;
lineNumber: number;
revision: string;
when: Date;
};
type Props = {
source: AnnotatedSource;
};
const Author = styled.a`
width: 8em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
float: left;
`;
const LineNumber = styled.span`
width: 3em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
border-left: 1px solid lightgrey;
border-right: 1px solid lightgrey;
text-align: right;
float: left;
padding: 0 0.5em;
`;
const When = styled.span`
width: 90px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
float: left;
margin: 0 0.5em;
`;
const Annotate: FC<Props> = ({ source }) => {
// @ts-ignore
const defaultRenderer = ({ rows, stylesheet, useInlineStyles }) => {
// @ts-ignore
return rows.map((node, i) => {
const line = createElement({
node,
stylesheet,
useInlineStyles,
key: `code-segement${i}`
});
if (i + 1 < rows.length) {
const annotation = source.lines[i];
return (
<span>
<Author>{annotation.author.name}</Author>{" "}
<When>
<DateShort value={annotation.when} />
</When>{" "}
<LineNumber>{i + 1}</LineNumber> {line}
</span>
);
}
return line;
});
};
const code = source.lines.reduce((content, line) => {
content += line.code + "\n";
return content;
}, "");
return (
<ReactSyntaxHighlighter
showLineNumbers={false}
language={source.language}
style={arduinoLight}
renderer={defaultRenderer}
>
{code}
</ReactSyntaxHighlighter>
);
};
export default Annotate;

View File

@@ -23,16 +23,14 @@
*/
import React from "react";
import { withTranslation, WithTranslation } from "react-i18next";
import { formatDistance, format, parseISO, Locale } from "date-fns";
import { formatDistance, format, Locale } from "date-fns";
import { enUS, de, es } from "date-fns/locale";
import styled from "styled-components";
import { DateInput, DateElement, FullDateFormat, toDate } from "./dates";
type LocaleMap = {
[key: string]: Locale;
};
type DateInput = Date | string;
export const supportedLocales: LocaleMap = {
enUS,
en: enUS,
@@ -59,11 +57,6 @@ type Options = {
timeZone?: string;
};
const DateElement = styled.time`
border-bottom: 1px dotted rgba(219, 219, 219);
cursor: help;
`;
export const chooseLocale = (language: string, languages?: string[]) => {
for (const lng of languages || []) {
const locale = supportedLocales[lng];
@@ -98,17 +91,10 @@ class DateFromNow extends React.Component<Props> {
return options;
};
toDate = (value: DateInput): Date => {
if (value instanceof Date) {
return value;
}
return parseISO(value);
};
getBaseDate = () => {
const { baseDate } = this.props;
if (baseDate) {
return this.toDate(baseDate);
return toDate(baseDate);
}
return new Date();
};
@@ -116,10 +102,10 @@ class DateFromNow extends React.Component<Props> {
render() {
const { date } = this.props;
if (date) {
const isoDate = this.toDate(date);
const isoDate = toDate(date);
const options = this.createOptions();
const distance = formatDistance(isoDate, this.getBaseDate(), options);
const formatted = format(isoDate, "yyyy-MM-dd HH:mm:ss", options);
const formatted = format(isoDate, FullDateFormat, options);
return <DateElement title={formatted}>{distance}</DateElement>;
}
return null;

View 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, { FC } from "react";
import { format } from "date-fns";
import { toDate, ShortDateFormat, FullDateFormat, DateElement } from "./dates";
type Props = {
value?: Date | string;
className?: string;
};
const DateShort: FC<Props> = ({ value, className }) => {
if (!value) {
return null;
}
const date = toDate(value);
return (
<DateElement className={className} title={format(date, FullDateFormat)}>
{format(date, ShortDateFormat)}
</DateElement>
);
};
export default DateShort;

View File

@@ -0,0 +1,19 @@
import styled from "styled-components";
import { parseISO } from "date-fns";
export type DateInput = Date | string;
export const ShortDateFormat = "yyyy-MM-dd";
export const FullDateFormat = "yyyy-MM-dd HH:mm:ss";
export const DateElement = styled.time`
border-bottom: 1px dotted rgba(219, 219, 219);
cursor: help;
`;
export const toDate = (value: DateInput): Date => {
if (value instanceof Date) {
return value;
}
return parseISO(value);
};

View File

@@ -43,7 +43,9 @@ import {
export { validation, urls, repositories };
export { default as Annotate } from "./Annotate";
export { default as DateFromNow } from "./DateFromNow";
export { default as DateShort } from "./DateShort";
export { default as ErrorNotification } from "./ErrorNotification";
export { default as ErrorPage } from "./ErrorPage";
export { default as Icon } from "./Icon";