diff --git a/scm-ui/ui-components/package.json b/scm-ui/ui-components/package.json index 7390623378..23ed2b76ba 100644 --- a/scm-ui/ui-components/package.json +++ b/scm-ui/ui-components/package.json @@ -60,7 +60,7 @@ "react-markdown": "^4.0.6", "react-router-dom": "^5.1.2", "react-select": "^2.1.2", - "react-syntax-highlighter": "^11.0.2" + "react-syntax-highlighter": "https://github.com/conorhastings/react-syntax-highlighter#08bcf49b1aa7877ce94f7208e73dfa6bef8b26e7" }, "babel": { "presets": [ diff --git a/scm-ui/ui-components/src/Annotate.stories.tsx b/scm-ui/ui-components/src/Annotate.stories.tsx new file mode 100644 index 0000000000..582c7545d0 --- /dev/null +++ b/scm-ui/ui-components/src/Annotate.stories.tsx @@ -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 => {storyFn()}) + .add("Default", () => ); diff --git a/scm-ui/ui-components/src/Annotate.tsx b/scm-ui/ui-components/src/Annotate.tsx new file mode 100644 index 0000000000..b71d78023b --- /dev/null +++ b/scm-ui/ui-components/src/Annotate.tsx @@ -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 = ({ 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 ( + + {annotation.author.name}{" "} + + + {" "} + {i + 1} {line} + + ); + } + + return line; + }); + }; + + const code = source.lines.reduce((content, line) => { + content += line.code + "\n"; + return content; + }, ""); + + return ( + + {code} + + ); +}; + +export default Annotate; diff --git a/scm-ui/ui-components/src/DateFromNow.tsx b/scm-ui/ui-components/src/DateFromNow.tsx index 6b68bd8bd2..eda6efdcc7 100644 --- a/scm-ui/ui-components/src/DateFromNow.tsx +++ b/scm-ui/ui-components/src/DateFromNow.tsx @@ -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 { 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 { 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 {distance}; } return null; diff --git a/scm-ui/ui-components/src/DateShort.tsx b/scm-ui/ui-components/src/DateShort.tsx new file mode 100644 index 0000000000..86510177ad --- /dev/null +++ b/scm-ui/ui-components/src/DateShort.tsx @@ -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 = ({ value, className }) => { + if (!value) { + return null; + } + const date = toDate(value); + return ( + + {format(date, ShortDateFormat)} + + ); +}; + +export default DateShort; diff --git a/scm-ui/ui-components/src/dates.ts b/scm-ui/ui-components/src/dates.ts new file mode 100644 index 0000000000..c335750ad2 --- /dev/null +++ b/scm-ui/ui-components/src/dates.ts @@ -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); +}; diff --git a/scm-ui/ui-components/src/index.ts b/scm-ui/ui-components/src/index.ts index 68ec1ec2ef..07f4bdf5f3 100644 --- a/scm-ui/ui-components/src/index.ts +++ b/scm-ui/ui-components/src/index.ts @@ -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"; diff --git a/yarn.lock b/yarn.lock index cb8829e58a..8651f444e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11809,7 +11809,7 @@ pretty-hrtime@^1.0.3: resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE= -prismjs@^1.8.4: +prismjs@^1.16.0, prismjs@^1.8.4: version "1.20.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.20.0.tgz#9b685fc480a3514ee7198eac6a3bf5024319ff03" integrity sha512-AEDjSrVNkynnw6A+B1DsFkd6AVdTnp+/WoUixFRULlCLZVRZlVQMVWio/16jv7G1FscUxQxOQhWwApgbnxr6kQ== @@ -12424,6 +12424,16 @@ react-syntax-highlighter@^11.0.2: prismjs "^1.8.4" refractor "^2.4.1" +"react-syntax-highlighter@https://github.com/conorhastings/react-syntax-highlighter#08bcf49b1aa7877ce94f7208e73dfa6bef8b26e7": + version "12.0.2" + resolved "https://github.com/conorhastings/react-syntax-highlighter#08bcf49b1aa7877ce94f7208e73dfa6bef8b26e7" + dependencies: + "@babel/runtime" "^7.3.1" + highlight.js "~9.13.0" + lowlight "~1.11.0" + prismjs "^1.16.0" + refractor "^2.10.1" + react-test-renderer@^16.0.0-0, react-test-renderer@^16.10.2: version "16.13.1" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.13.1.tgz#de25ea358d9012606de51e012d9742e7f0deabc1" @@ -12736,7 +12746,7 @@ reflect.ownkeys@^0.2.0: resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" integrity sha1-dJrO7H8/34tj+SegSAnpDFwLNGA= -refractor@^2.4.1: +refractor@^2.10.1, refractor@^2.4.1: version "2.10.1" resolved "https://registry.yarnpkg.com/refractor/-/refractor-2.10.1.tgz#166c32f114ed16fd96190ad21d5193d3afc7d34e" integrity sha512-Xh9o7hQiQlDbxo5/XkOX6H+x/q8rmlmZKr97Ie1Q8ZM32IRRd3B/UxuA/yXDW79DBSXGWxm2yRTbcTVmAciJRw==