From a95ececec9bb4f7c4d7ddb6c9541afddc4ac3d10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 30 Jun 2020 10:28:22 +0200 Subject: [PATCH 01/15] Add method to replace parts in texts --- .../changesets/textSplitAndReplace.test.ts | 101 ++++++++++++++++++ .../changesets/textSplitAndReplace.ts | 75 +++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 scm-ui/ui-webapp/src/repos/components/changesets/textSplitAndReplace.test.ts create mode 100644 scm-ui/ui-webapp/src/repos/components/changesets/textSplitAndReplace.ts diff --git a/scm-ui/ui-webapp/src/repos/components/changesets/textSplitAndReplace.test.ts b/scm-ui/ui-webapp/src/repos/components/changesets/textSplitAndReplace.test.ts new file mode 100644 index 0000000000..641ff2cb9b --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/components/changesets/textSplitAndReplace.test.ts @@ -0,0 +1,101 @@ +/* + * 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 textSplitAndReplace from "./textSplitAndReplace"; + +type Wrapped = { + text: string; +}; + +const testWrapper = (s: string) => { + return { text: s }; +}; + +describe("text split and replace", () => { + it("should wrap text if nothing should be replaced", () => { + const result = textSplitAndReplace("Don't Panic.", [], testWrapper); + expect(result).toHaveLength(1); + expect(result[0]).toStrictEqual({ text: "Don't Panic." }); + }); + + it("should replace single string", () => { + const result = textSplitAndReplace( + "Don't Panic.", + [{ textToReplace: "'", replacement: { text: "`" } }], + testWrapper + ); + expect(result).toHaveLength(3); + expect(result[0]).toStrictEqual({ text: "Don" }); + expect(result[1]).toStrictEqual({ text: "`" }); + expect(result[2]).toStrictEqual({ text: "t Panic." }); + }); + + it("should replace strings only once if replace all is not set", () => { + const result = textSplitAndReplace( + "'So this is it,' said Arthur, 'We are going to die.'", + [{ textToReplace: "'", replacement: { text: "“" } }], + testWrapper + ); + expect(result).toHaveLength(2); + expect(result[0]).toStrictEqual({ text: "“" }); + expect(result[1]).toStrictEqual({ text: "So this is it,' said Arthur, 'We are going to die.'" }); + }); + + it("should replace all strings if replace all is set to true", () => { + const result = textSplitAndReplace( + "'So this is it,' said Arthur, 'We are going to die.'", + [{ textToReplace: "'", replacement: { text: "“" }, replaceAll: true }], + testWrapper + ); + expect(result).toHaveLength(7); + expect(result[0]).toStrictEqual({ text: "“" }); + expect(result[1]).toStrictEqual({ text: "So this is it," }); + expect(result[2]).toStrictEqual({ text: "“" }); + expect(result[3]).toStrictEqual({ text: " said Arthur, " }); + expect(result[4]).toStrictEqual({ text: "“" }); + expect(result[5]).toStrictEqual({ text: "We are going to die." }); + expect(result[6]).toStrictEqual({ text: "“" }); + }); + + it("should replace strings with multiple replacements", () => { + const result = textSplitAndReplace( + "'So this is it,' said Arthur, 'We are going to die.'", + [ + { textToReplace: "'", replacement: { text: "“" }, replaceAll: true }, + { textToReplace: "Arthur", replacement: { text: "Dent" }, replaceAll: true } + ], + testWrapper + ); + expect(result).toHaveLength(9); + expect(result[0]).toStrictEqual({ text: "“" }); + expect(result[1]).toStrictEqual({ text: "So this is it," }); + expect(result[2]).toStrictEqual({ text: "“" }); + expect(result[3]).toStrictEqual({ text: " said " }); + expect(result[4]).toStrictEqual({ text: "Dent" }); + expect(result[5]).toStrictEqual({ text: ", " }); + expect(result[6]).toStrictEqual({ text: "“" }); + expect(result[7]).toStrictEqual({ text: "We are going to die." }); + expect(result[8]).toStrictEqual({ text: "“" }); + }); +}); diff --git a/scm-ui/ui-webapp/src/repos/components/changesets/textSplitAndReplace.ts b/scm-ui/ui-webapp/src/repos/components/changesets/textSplitAndReplace.ts new file mode 100644 index 0000000000..1f4bcf3fad --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/components/changesets/textSplitAndReplace.ts @@ -0,0 +1,75 @@ +/* + * 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. + */ + +type Replacement = { + textToReplace: string; + replacement: T; + replaceAll?: boolean; +}; + +type PartToReplace = { + start: number; + length: number; + replacement: T; +}; + +export default function textSplitAndReplace( + text: string, + replacements: Replacement[], + textWrapper: (s: string) => T +): T[] { + const partsToReplace: PartToReplace[] = []; + + replacements.forEach(replacement => { + let lastIndex = -1; + do { + const start = text.indexOf(replacement.textToReplace, lastIndex); + if (start >= 0) { + const length = replacement.textToReplace.length; + partsToReplace.push({ start, length, replacement: replacement.replacement }); + lastIndex = start + length; + } else { + lastIndex = -1; + } + } while (replacement.replaceAll && lastIndex >= 0); + }); + + partsToReplace.sort((a, b) => a.start - b.start); + + const result: T[] = []; + + let lastIndex = 0; + for (const { start, length, replacement } of partsToReplace) { + if (start > lastIndex) { + result.push(textWrapper(text.substr(lastIndex, start - lastIndex))); + } + result.push(replacement); + lastIndex = start + length; + } + if (lastIndex < text.length) { + result.push(textWrapper(text.substr(lastIndex))); + } + + return result; +} From 6df62788a1431646bdc427df0f316b508c4b88d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 30 Jun 2020 11:00:36 +0200 Subject: [PATCH 02/15] Handle overlapping replacements --- .../changesets/textSplitAndReplace.test.ts | 33 +++++++++++++++++++ .../changesets/textSplitAndReplace.ts | 13 +++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/scm-ui/ui-webapp/src/repos/components/changesets/textSplitAndReplace.test.ts b/scm-ui/ui-webapp/src/repos/components/changesets/textSplitAndReplace.test.ts index 641ff2cb9b..778236ee6d 100644 --- a/scm-ui/ui-webapp/src/repos/components/changesets/textSplitAndReplace.test.ts +++ b/scm-ui/ui-webapp/src/repos/components/changesets/textSplitAndReplace.test.ts @@ -98,4 +98,37 @@ describe("text split and replace", () => { expect(result[7]).toStrictEqual({ text: "We are going to die." }); expect(result[8]).toStrictEqual({ text: "“" }); }); + + it("should ignore conflicting replacements", () => { + const result = textSplitAndReplace( + "'So this is it,' said Arthur, 'We are going to die.'", + [ + { textToReplace: "said Arthur", replacement: { text: "to be replaced" } }, + { textToReplace: " said", replacement: { text: "to be ignored 1" }, replaceAll: true }, + { textToReplace: "d A", replacement: { text: "to be ignored 2" }, replaceAll: true }, + { textToReplace: "Arthur,", replacement: { text: "to be ignored 3" }, replaceAll: true } + ], + testWrapper + ); + expect(result).toHaveLength(3); + expect(result[0]).toStrictEqual({ text: "'So this is it,' " }); + expect(result[1]).toStrictEqual({ text: "to be replaced" }); + expect(result[2]).toStrictEqual({ text: ", 'We are going to die.'" }); + }); + + it("should replace adjacent texts", () => { + const result = textSplitAndReplace( + "'So this is it,' said Arthur, 'We are going to die.'", + [ + { textToReplace: "'So this is it,'", replacement: { text: "one" } }, + { textToReplace: " said Arthur, ", replacement: { text: "two" } }, + { textToReplace: "'We are going to die.'", replacement: { text: "three" } } + ], + testWrapper + ); + expect(result).toHaveLength(3); + expect(result[0]).toStrictEqual({ text: "one" }); + expect(result[1]).toStrictEqual({ text: "two" }); + expect(result[2]).toStrictEqual({ text: "three" }); + }); }); diff --git a/scm-ui/ui-webapp/src/repos/components/changesets/textSplitAndReplace.ts b/scm-ui/ui-webapp/src/repos/components/changesets/textSplitAndReplace.ts index 1f4bcf3fad..c94f85f34c 100644 --- a/scm-ui/ui-webapp/src/repos/components/changesets/textSplitAndReplace.ts +++ b/scm-ui/ui-webapp/src/repos/components/changesets/textSplitAndReplace.ts @@ -34,6 +34,14 @@ type PartToReplace = { replacement: T; }; +function hasConflict(alreadyFoundReplacements: PartToReplace[], newReplacement: PartToReplace) { + return !!alreadyFoundReplacements.find( + existing => + (existing.start <= newReplacement.start && existing.start + existing.length > newReplacement.start) || + (newReplacement.start <= existing.start && newReplacement.start + newReplacement.length > existing.start) + ); +} + export default function textSplitAndReplace( text: string, replacements: Replacement[], @@ -47,7 +55,10 @@ export default function textSplitAndReplace( const start = text.indexOf(replacement.textToReplace, lastIndex); if (start >= 0) { const length = replacement.textToReplace.length; - partsToReplace.push({ start, length, replacement: replacement.replacement }); + const newReplacement = { start, length, replacement: replacement.replacement }; + if (!hasConflict(partsToReplace, newReplacement)) { + partsToReplace.push(newReplacement); + } lastIndex = start + length; } else { lastIndex = -1; From f62683e528a95bc124e742ad700079db48498b3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 30 Jun 2020 15:20:59 +0200 Subject: [PATCH 03/15] Create split and replace component --- .../src/SplitAndReplace.stories.tsx | 59 +++++++++++++++++++ scm-ui/ui-components/src/SplitAndReplace.tsx | 52 ++++++++++++++++ .../src}/textSplitAndReplace.test.ts | 0 .../src}/textSplitAndReplace.ts | 0 4 files changed, 111 insertions(+) create mode 100644 scm-ui/ui-components/src/SplitAndReplace.stories.tsx create mode 100644 scm-ui/ui-components/src/SplitAndReplace.tsx rename scm-ui/{ui-webapp/src/repos/components/changesets => ui-components/src}/textSplitAndReplace.test.ts (100%) rename scm-ui/{ui-webapp/src/repos/components/changesets => ui-components/src}/textSplitAndReplace.ts (100%) diff --git a/scm-ui/ui-components/src/SplitAndReplace.stories.tsx b/scm-ui/ui-components/src/SplitAndReplace.stories.tsx new file mode 100644 index 0000000000..a49f760c87 --- /dev/null +++ b/scm-ui/ui-components/src/SplitAndReplace.stories.tsx @@ -0,0 +1,59 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import React from "react"; +import { storiesOf } from "@storybook/react"; +import SplitAndReplace from "./SplitAndReplace"; +import { Icon } from "@scm-manager/ui-components/src"; + +storiesOf("SplitAndReplace", module).add("Simple replacement", () => { + const replacements = [ + { + textToReplace: "'", + replacement: , + replaceAll: true + }, + { + textToReplace: "`", + replacement: , + replaceAll: true + }, + { + replacement:
 
, + textToReplace: " ", + replaceAll: true + } + ]; + return ( + <> +
+
+ + ); +}); diff --git a/scm-ui/ui-components/src/SplitAndReplace.tsx b/scm-ui/ui-components/src/SplitAndReplace.tsx new file mode 100644 index 0000000000..213e9ff5d5 --- /dev/null +++ b/scm-ui/ui-components/src/SplitAndReplace.tsx @@ -0,0 +1,52 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import React, { FC, ReactNode } from "react"; +import textSplitAndReplace from "./textSplitAndReplace"; + +type Replacement = { + textToReplace: string; + replacement: ReactNode; + replaceAll: boolean; +}; + +type Props = { + text: string; + replacements: Replacement[]; +}; + +type PartToReplace = { + start: number; + length: number; + replacement: ReactNode; +}; + +const SplitAndReplace: FC = ({ text, replacements }) => { + const parts = textSplitAndReplace(text, replacements, s =>
{s}
); + if (parts.length === 0) { + return <>{parts[0]}; + } + return
{parts}
; +}; + +export default SplitAndReplace; diff --git a/scm-ui/ui-webapp/src/repos/components/changesets/textSplitAndReplace.test.ts b/scm-ui/ui-components/src/textSplitAndReplace.test.ts similarity index 100% rename from scm-ui/ui-webapp/src/repos/components/changesets/textSplitAndReplace.test.ts rename to scm-ui/ui-components/src/textSplitAndReplace.test.ts diff --git a/scm-ui/ui-webapp/src/repos/components/changesets/textSplitAndReplace.ts b/scm-ui/ui-components/src/textSplitAndReplace.ts similarity index 100% rename from scm-ui/ui-webapp/src/repos/components/changesets/textSplitAndReplace.ts rename to scm-ui/ui-components/src/textSplitAndReplace.ts From 53993cfee7d41c9ef24215d947751f748721a224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 30 Jun 2020 18:11:23 +0200 Subject: [PATCH 04/15] Introduce new extension point for changeset description This new extension point will only be rendered when the old extension point is not bound. --- scm-ui/ui-components/src/SplitAndReplace.tsx | 4 +- .../src/__resources__/changesets.tsx | 50 ++++++++++++++++++- scm-ui/ui-components/src/index.ts | 1 + .../repos/changesets/ChangesetDescription.tsx | 45 +++++++++++++++++ .../src/repos/changesets/ChangesetRow.tsx | 15 +++--- .../repos/changesets/Changesets.stories.tsx | 48 +++++++++++------- .../src/repos/changesets/index.ts | 1 + .../changesets/ChangesetDetails.tsx | 7 +-- 8 files changed, 141 insertions(+), 30 deletions(-) create mode 100644 scm-ui/ui-components/src/repos/changesets/ChangesetDescription.tsx diff --git a/scm-ui/ui-components/src/SplitAndReplace.tsx b/scm-ui/ui-components/src/SplitAndReplace.tsx index 213e9ff5d5..9db88e9aaa 100644 --- a/scm-ui/ui-components/src/SplitAndReplace.tsx +++ b/scm-ui/ui-components/src/SplitAndReplace.tsx @@ -24,10 +24,10 @@ import React, { FC, ReactNode } from "react"; import textSplitAndReplace from "./textSplitAndReplace"; -type Replacement = { +export type Replacement = { textToReplace: string; replacement: ReactNode; - replaceAll: boolean; + replaceAll?: boolean; }; type Props = { diff --git a/scm-ui/ui-components/src/__resources__/changesets.tsx b/scm-ui/ui-components/src/__resources__/changesets.tsx index e53f6a9545..2743378ad4 100644 --- a/scm-ui/ui-components/src/__resources__/changesets.tsx +++ b/scm-ui/ui-components/src/__resources__/changesets.tsx @@ -218,6 +218,54 @@ const four: Changeset = { } }; +const five: Changeset = { + id: "d21cc6c359270aef2196796f4d96af65f51866dc", + author: { mail: "scm-admin@scm-manager.org", name: "SCM Administrator" }, + date: new Date("2020-06-09T05:39:50Z"), + description: "HOG-42 Change mail to arthur@guide.galaxy\n\n", + _links: { + self: { + href: + "http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/changesets/d21cc6c359270aef2196796f4d96af65f51866dc" + }, + diff: { + href: + "http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/diff/d21cc6c359270aef2196796f4d96af65f51866dc" + }, + sources: { + href: + "http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/sources/d21cc6c359270aef2196796f4d96af65f51866dc" + }, + modifications: { + href: + "http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/modifications/d21cc6c359270aef2196796f4d96af65f51866dc" + }, + diffParsed: { + href: + "http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/diff/d21cc6c359270aef2196796f4d96af65f51866dc/parsed" + } + }, + _embedded: { + tags: [], + branches: [], + parents: [ + { + id: "e163c8f632db571c9aa51a8eb440e37cf550b825", + _links: { + self: { + href: + "http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/changesets/e163c8f632db571c9aa51a8eb440e37cf550b825" + }, + diff: { + href: + "http://localhost:8081/scm/api/v2/repositories/hitchhiker/heart-of-gold/diff/e163c8f632db571c9aa51a8eb440e37cf550b825" + } + } + } + ] + } +}; + const changesets: PagedCollection = { page: 0, pageTotal: 1, @@ -246,5 +294,5 @@ const changesets: PagedCollection = { } }; -export { one, two, three, four }; +export { one, two, three, four, five }; export default changesets; diff --git a/scm-ui/ui-components/src/index.ts b/scm-ui/ui-components/src/index.ts index 19ba35a1f4..9a8049a7bd 100644 --- a/scm-ui/ui-components/src/index.ts +++ b/scm-ui/ui-components/src/index.ts @@ -78,6 +78,7 @@ export { default as CardColumnGroup } from "./CardColumnGroup"; export { default as CardColumn } from "./CardColumn"; export { default as CardColumnSmall } from "./CardColumnSmall"; export { default as CommaSeparatedList } from "./CommaSeparatedList"; +export { default as SplitAndReplace, Replacement } from "./SplitAndReplace"; export { default as comparators } from "./comparators"; diff --git a/scm-ui/ui-components/src/repos/changesets/ChangesetDescription.tsx b/scm-ui/ui-components/src/repos/changesets/ChangesetDescription.tsx new file mode 100644 index 0000000000..0e80212a27 --- /dev/null +++ b/scm-ui/ui-components/src/repos/changesets/ChangesetDescription.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 { Changeset } from "@scm-manager/ui-types"; +import { useBinder } from "@scm-manager/ui-extensions"; +import { SplitAndReplace, Replacement } from "@scm-manager/ui-components"; + +type Props = { + changeset: Changeset; + value: string; +}; + +const ChangesetDescription: FC = ({ changeset, value }) => { + const binder = useBinder(); + + const replacements: Replacement[][] = binder.getExtensions("changeset.description.tokens", { + changeset, + value + }); + return r)} />; +}; + +export default ChangesetDescription; diff --git a/scm-ui/ui-components/src/repos/changesets/ChangesetRow.tsx b/scm-ui/ui-components/src/repos/changesets/ChangesetRow.tsx index 0956f7f131..46a30a6fd4 100644 --- a/scm-ui/ui-components/src/repos/changesets/ChangesetRow.tsx +++ b/scm-ui/ui-components/src/repos/changesets/ChangesetRow.tsx @@ -34,6 +34,7 @@ import ChangesetId from "./ChangesetId"; import ChangesetAuthor from "./ChangesetAuthor"; import ChangesetTags from "./ChangesetTags"; import ChangesetButtonGroup from "./ChangesetButtonGroup"; +import ChangesetDescription from "./ChangesetDescription"; type Props = WithTranslation & { repository: Repository; @@ -100,7 +101,7 @@ class ChangesetRow extends React.Component { - + @@ -114,28 +115,28 @@ class ChangesetRow extends React.Component { }} renderAll={false} > - {description.title} +

- +

- +

- + - + - + { return `https://robohash.org/${person.mail}`; -} +}; const withAvatarFactory = (factory: (person: Person) => string, changeset: Changeset) => { const binder = new Binder("changeset stories"); @@ -53,21 +54,23 @@ const withAvatarFactory = (factory: (person: Person) => string, changeset: Chang ); }; +const withReplacements = (replacements: Replacement[][], changeset: Changeset) => { + const binder = new Binder("changeset stories"); + replacements.forEach(replacement => binder.bind("changeset.description.tokens", replacement)); + return ( + + + + ); +}; + storiesOf("Changesets", module) .addDecorator(story => {story()}) .addDecorator(storyFn => {storyFn()}) - .add("Default", () => ( - - )) - .add("With Committer", () => ( - - )) - .add("With Committer and Co-Author", () => ( - - )) - .add("With multiple Co-Authors", () => ( - - )) + .add("Default", () => ) + .add("With Committer", () => ) + .add("With Committer and Co-Author", () => ) + .add("With multiple Co-Authors", () => ) .add("With avatar", () => { return withAvatarFactory(person => hitchhiker, three); }) @@ -76,4 +79,15 @@ storiesOf("Changesets", module) }) .add("Co-Authors with avatar", () => { return withAvatarFactory(robohash, four); + }) + .add("Replacements", () => { + const link = HOG-42; + const mail = Arthur; + return withReplacements( + [ + [{ textToReplace: "HOG-42", replacement: link }], + [{ textToReplace: "arthur@guide.galaxy", replacement: mail }] + ], + five + ); }); diff --git a/scm-ui/ui-components/src/repos/changesets/index.ts b/scm-ui/ui-components/src/repos/changesets/index.ts index b4c83dd4c3..3fb6b39c72 100644 --- a/scm-ui/ui-components/src/repos/changesets/index.ts +++ b/scm-ui/ui-components/src/repos/changesets/index.ts @@ -27,6 +27,7 @@ export { changesets }; export { default as ChangesetAuthor, SingleContributor } from "./ChangesetAuthor"; export { default as ChangesetButtonGroup } from "./ChangesetButtonGroup"; +export { default as ChangesetDescription } from "./ChangesetDescription"; export { default as ChangesetDiff } from "./ChangesetDiff"; export { default as ChangesetId } from "./ChangesetId"; export { default as ChangesetList } from "./ChangesetList"; diff --git a/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetDetails.tsx b/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetDetails.tsx index 6ba44c0c1d..657c47249d 100644 --- a/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetDetails.tsx +++ b/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetDetails.tsx @@ -32,6 +32,7 @@ import { AvatarWrapper, Button, ChangesetAuthor, + ChangesetDescription, ChangesetDiff, ChangesetId, changesets, @@ -106,7 +107,7 @@ const ContributorToggleLine = styled.p` const ChangesetSummary = styled.div` display: flex; justify-content: space-between; -` +`; const SeparatedParents = styled.div` margin-left: 1em; @@ -180,7 +181,7 @@ class ChangesetDetails extends React.Component { }} renderAll={false} > - {description.title} +
@@ -217,7 +218,7 @@ class ChangesetDetails extends React.Component { }} renderAll={false} > - {item} +
From 8a0e0a3cc5e0996d3f6d18985778ed74389dbc0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 1 Jul 2020 10:43:17 +0200 Subject: [PATCH 05/15] Add extension point for changeset short links. --- .../src/SplitAndReplace.stories.tsx | 27 ++++------ scm-ui/ui-components/src/SplitAndReplace.tsx | 16 ++++-- .../repos/changesets/ChangesetDescription.tsx | 14 ++++-- .../repos/changesets/Changesets.stories.tsx | 11 +++-- scm-ui/ui-webapp/src/index.tsx | 4 ++ .../changesets/ChangesetShortLink.tsx | 49 +++++++++++++++++++ 6 files changed, 90 insertions(+), 31 deletions(-) create mode 100644 scm-ui/ui-webapp/src/repos/components/changesets/ChangesetShortLink.tsx diff --git a/scm-ui/ui-components/src/SplitAndReplace.stories.tsx b/scm-ui/ui-components/src/SplitAndReplace.stories.tsx index a49f760c87..69b81cc12e 100644 --- a/scm-ui/ui-components/src/SplitAndReplace.stories.tsx +++ b/scm-ui/ui-components/src/SplitAndReplace.stories.tsx @@ -24,36 +24,29 @@ import React from "react"; import { storiesOf } from "@storybook/react"; import SplitAndReplace from "./SplitAndReplace"; -import { Icon } from "@scm-manager/ui-components/src"; +import { Icon } from "@scm-manager/ui-components"; storiesOf("SplitAndReplace", module).add("Simple replacement", () => { const replacements = [ { textToReplace: "'", - replacement: , + replacement: , replaceAll: true }, { textToReplace: "`", - replacement: , - replaceAll: true - }, - { - replacement:
 
, - textToReplace: " ", + replacement: , replaceAll: true } ]; return ( <> -
-
- +
+ +
+
+ +
+ ); }); diff --git a/scm-ui/ui-components/src/SplitAndReplace.tsx b/scm-ui/ui-components/src/SplitAndReplace.tsx index 9db88e9aaa..8fc1269e72 100644 --- a/scm-ui/ui-components/src/SplitAndReplace.tsx +++ b/scm-ui/ui-components/src/SplitAndReplace.tsx @@ -35,14 +35,20 @@ type Props = { replacements: Replacement[]; }; -type PartToReplace = { - start: number; - length: number; - replacement: ReactNode; +const textWrapper = (s: string) => { + const first = s.startsWith(" ") ? <>  : ""; + const last = s.endsWith(" ") ? <>  : ""; + return ( +
+ {first} + {s} + {last} +
+ ); }; const SplitAndReplace: FC = ({ text, replacements }) => { - const parts = textSplitAndReplace(text, replacements, s =>
{s}
); + const parts = textSplitAndReplace(text, replacements, textWrapper); if (parts.length === 0) { return <>{parts[0]}; } diff --git a/scm-ui/ui-components/src/repos/changesets/ChangesetDescription.tsx b/scm-ui/ui-components/src/repos/changesets/ChangesetDescription.tsx index 0e80212a27..3a76689eac 100644 --- a/scm-ui/ui-components/src/repos/changesets/ChangesetDescription.tsx +++ b/scm-ui/ui-components/src/repos/changesets/ChangesetDescription.tsx @@ -35,11 +35,15 @@ type Props = { const ChangesetDescription: FC = ({ changeset, value }) => { const binder = useBinder(); - const replacements: Replacement[][] = binder.getExtensions("changeset.description.tokens", { - changeset, - value - }); - return r)} />; + const replacements: ((changeset: Changeset, value: string) => Replacement[])[] = binder.getExtensions( + "changeset.description.tokens", + { + changeset, + value + } + ); + + return r(changeset, value))} />; }; export default ChangesetDescription; diff --git a/scm-ui/ui-components/src/repos/changesets/Changesets.stories.tsx b/scm-ui/ui-components/src/repos/changesets/Changesets.stories.tsx index c895ab8ee9..2c4f1e48f3 100644 --- a/scm-ui/ui-components/src/repos/changesets/Changesets.stories.tsx +++ b/scm-ui/ui-components/src/repos/changesets/Changesets.stories.tsx @@ -54,7 +54,10 @@ const withAvatarFactory = (factory: (person: Person) => string, changeset: Chang ); }; -const withReplacements = (replacements: Replacement[][], changeset: Changeset) => { +const withReplacements = ( + replacements: ((changeset: Changeset, value: string) => Replacement[])[], + changeset: Changeset +) => { const binder = new Binder("changeset stories"); replacements.forEach(replacement => binder.bind("changeset.description.tokens", replacement)); return ( @@ -72,7 +75,7 @@ storiesOf("Changesets", module) .add("With Committer and Co-Author", () => ) .add("With multiple Co-Authors", () => ) .add("With avatar", () => { - return withAvatarFactory(person => hitchhiker, three); + return withAvatarFactory(() => hitchhiker, three); }) .add("Commiter and Co-Authors with avatar", () => { return withAvatarFactory(robohash, one); @@ -85,8 +88,8 @@ storiesOf("Changesets", module) const mail = Arthur; return withReplacements( [ - [{ textToReplace: "HOG-42", replacement: link }], - [{ textToReplace: "arthur@guide.galaxy", replacement: mail }] + () => [{ textToReplace: "HOG-42", replacement: link }], + () => [{ textToReplace: "arthur@guide.galaxy", replacement: mail }] ], five ); diff --git a/scm-ui/ui-webapp/src/index.tsx b/scm-ui/ui-webapp/src/index.tsx index 25a2ea0ea3..519b0c7951 100644 --- a/scm-ui/ui-webapp/src/index.tsx +++ b/scm-ui/ui-webapp/src/index.tsx @@ -34,6 +34,10 @@ import createReduxStore from "./createReduxStore"; import { BrowserRouter as Router } from "react-router-dom"; import { urls } from "@scm-manager/ui-components"; +import { binder } from "@scm-manager/ui-extensions"; +import ChangesetShortLink from "./repos/components/changesets/ChangesetShortLink"; + +binder.bind("changeset.description.tokens", ChangesetShortLink); const store = createReduxStore(); diff --git a/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetShortLink.tsx b/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetShortLink.tsx new file mode 100644 index 0000000000..13a129fc23 --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetShortLink.tsx @@ -0,0 +1,49 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React from "react"; +import { Link } from "react-router-dom"; +import { Changeset } from "@scm-manager/ui-types"; +import { Replacement } from "@scm-manager/ui-components"; + +const ChangesetShortLink: (changeset: Changeset, value: string) => Replacement[] = (changeset, value) => { + const matches = value.match(/(\w+)\/(\w+)@([a-f0-9]+)/g); + if (!matches) { + return []; + } + return matches.map(value => { + const groups = value.match(/(\w+)\/(\w+)@([a-f0-9]+)/); + const namespace = groups[1]; + const name = groups[2]; + const revision = groups[3]; + const link = `/repo/${namespace}/${name}/changeset/${revision}`; + const replacement: Replacement = { + textToReplace: value, + replacement: {value} + }; + return replacement; + }); +}; + +export default ChangesetShortLink; From 19cf6be4c69a6a0869b7502a6a70e0960a76d443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 1 Jul 2020 11:17:28 +0200 Subject: [PATCH 06/15] Adapt to valid characters for namespace and name --- .../src/repos/components/changesets/ChangesetShortLink.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetShortLink.tsx b/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetShortLink.tsx index 13a129fc23..e4f03a351f 100644 --- a/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetShortLink.tsx +++ b/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetShortLink.tsx @@ -28,12 +28,13 @@ import { Changeset } from "@scm-manager/ui-types"; import { Replacement } from "@scm-manager/ui-components"; const ChangesetShortLink: (changeset: Changeset, value: string) => Replacement[] = (changeset, value) => { - const matches = value.match(/(\w+)\/(\w+)@([a-f0-9]+)/g); + const linkRegExp = "([A-Za-z0-9\.\-_]+)\/([A-Za-z0-9\.\-_]+)@([a-f0-9]+)"; + const matches = value.match(new RegExp(linkRegExp, "g")); if (!matches) { return []; } return matches.map(value => { - const groups = value.match(/(\w+)\/(\w+)@([a-f0-9]+)/); + const groups = value.match(new RegExp(linkRegExp)); const namespace = groups[1]; const name = groups[2]; const revision = groups[3]; From 1c71e9e6895d8572933af0b35ecf9b3d7aaad94c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 1 Jul 2020 15:44:13 +0200 Subject: [PATCH 07/15] Log changes --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09947917d0..4fdf6874ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Added - enrich commit mentions in markdown viewer by internal links ([#1210](https://github.com/scm-manager/scm-manager/pull/1210)) +- New extension point `changeset.description.tokens` to "enrich" commit messages ([#1231](https://github.com/scm-manager/scm-manager/pull/1231)) ### Changed - Checkboxes can now be 'indeterminate' ([#1215](https://github.com/scm-manager/scm-manager/pull/1215)) +- The old frontend extension point `changeset.description` is deprecated and should be replaced with `changeset.description.tokens` ([#1231](https://github.com/scm-manager/scm-manager/pull/1231)) ### Fixed - Fixed installation of debian packages on distros without preinstalled `at` ([#1216](https://github.com/scm-manager/scm-manager/issues/1216) and [#1217](https://github.com/scm-manager/scm-manager/pull/1217)) From 0387b7aa5e04631ed2f9b6daad3869b44db3e6b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 2 Jul 2020 08:14:26 +0200 Subject: [PATCH 08/15] Describe changed extension point --- docs/en/development/plugins/extension-points.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/en/development/plugins/extension-points.md b/docs/en/development/plugins/extension-points.md index 43492a2931..6a828f66bc 100644 --- a/docs/en/development/plugins/extension-points.md +++ b/docs/en/development/plugins/extension-points.md @@ -7,7 +7,12 @@ The following extension points are provided for the frontend: ### admin.navigation ### admin.route ### admin.setting -### changeset.description +### changeset.description.tokes +- Can be used to replace parts of a changeset description with components +- Has to be bound with a funktion taking the changeset and the (partial) description and returning `Replacement` objects with the following attributes: + - textToReplace: The text part of the description that should be replaced by a component + - replacement: The component to take instead of the text to replace + - replaceAll: Optional boolean; if set to `true`, all occurances of the text will be replaced (default: `false`) ### changeset.right ### changesets.author.suffix ### group.navigation @@ -56,6 +61,8 @@ The following extension points are provided for the frontend: # Deprecated +### changeset.description + ### changeset.avatar-factory - Location: At every changeset (detailed view as well as changeset overview) - can be used to add avatar (such as gravatar) for each changeset @@ -72,7 +79,7 @@ The following extension points are provided for the frontend: ### markdown-renderer-factory - A Factory function to create markdown [renderer](https://github.com/rexxars/react-markdown#node-types) - The factory function will be called with a renderContext parameter of type Object. this parameter is given as a prop for the MarkdownView component. - + **example:** From 08aa6fafffc4ca7eaa91e54eeeba184d68c0d7e2 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 2 Jul 2020 10:53:48 +0200 Subject: [PATCH 09/15] fixed typo --- docs/en/development/plugins/extension-points.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/development/plugins/extension-points.md b/docs/en/development/plugins/extension-points.md index 6a828f66bc..74893cfac4 100644 --- a/docs/en/development/plugins/extension-points.md +++ b/docs/en/development/plugins/extension-points.md @@ -7,7 +7,7 @@ The following extension points are provided for the frontend: ### admin.navigation ### admin.route ### admin.setting -### changeset.description.tokes +### changeset.description.tokens - Can be used to replace parts of a changeset description with components - Has to be bound with a funktion taking the changeset and the (partial) description and returning `Replacement` objects with the following attributes: - textToReplace: The text part of the description that should be replaced by a component From c352f9a751e1a64297d02f0e1351315c7c4114dd Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 2 Jul 2020 10:54:06 +0200 Subject: [PATCH 10/15] added deprecation mark --- docs/en/development/plugins/extension-points.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/en/development/plugins/extension-points.md b/docs/en/development/plugins/extension-points.md index 74893cfac4..038209f88c 100644 --- a/docs/en/development/plugins/extension-points.md +++ b/docs/en/development/plugins/extension-points.md @@ -62,6 +62,9 @@ The following extension points are provided for the frontend: # Deprecated ### changeset.description +- can be used to replace the whole description of a changeset + +**Deprecated:** Use `changeset.description.tokens` instead ### changeset.avatar-factory - Location: At every changeset (detailed view as well as changeset overview) From 8c67f6f04882b4d79676cdcf325d2d0793b8c359 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 2 Jul 2020 10:54:24 +0200 Subject: [PATCH 11/15] removed debug logging --- scm-ui/ui-components/src/remarkCommitLinksParser.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/scm-ui/ui-components/src/remarkCommitLinksParser.test.ts b/scm-ui/ui-components/src/remarkCommitLinksParser.test.ts index 9d54e3539a..a0af2aeee9 100644 --- a/scm-ui/ui-components/src/remarkCommitLinksParser.test.ts +++ b/scm-ui/ui-components/src/remarkCommitLinksParser.test.ts @@ -58,8 +58,6 @@ describe("Remark Commit Links RegEx Tests", () => { match = regExp.exec(text); } - console.log(matches) - expect(matches[0]).toBe("hitchhiker/heart-of-gold@42"); expect(matches[1]).toBe("hitchhiker/heart-of-gold@21"); }); From e8eaaf486f1a7dc7b9b9e4d0582575de4b1fdc94 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 2 Jul 2020 11:27:46 +0200 Subject: [PATCH 12/15] use fragments instead of div to avoid layout problems --- .../ui-components/src/SplitAndReplace.stories.tsx | 13 +++++++++---- scm-ui/ui-components/src/SplitAndReplace.tsx | 6 +++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/scm-ui/ui-components/src/SplitAndReplace.stories.tsx b/scm-ui/ui-components/src/SplitAndReplace.stories.tsx index 69b81cc12e..a6d40a5195 100644 --- a/scm-ui/ui-components/src/SplitAndReplace.stories.tsx +++ b/scm-ui/ui-components/src/SplitAndReplace.stories.tsx @@ -25,6 +25,11 @@ import React from "react"; import { storiesOf } from "@storybook/react"; import SplitAndReplace from "./SplitAndReplace"; import { Icon } from "@scm-manager/ui-components"; +import styled from "styled-components"; + +const Wrapper = styled.div` + margin: 2rem; +`; storiesOf("SplitAndReplace", module).add("Simple replacement", () => { const replacements = [ @@ -41,12 +46,12 @@ storiesOf("SplitAndReplace", module).add("Simple replacement", () => { ]; return ( <> -
+ -
-
+ + -
+ ); }); diff --git a/scm-ui/ui-components/src/SplitAndReplace.tsx b/scm-ui/ui-components/src/SplitAndReplace.tsx index 8fc1269e72..a3f72b553f 100644 --- a/scm-ui/ui-components/src/SplitAndReplace.tsx +++ b/scm-ui/ui-components/src/SplitAndReplace.tsx @@ -39,11 +39,11 @@ const textWrapper = (s: string) => { const first = s.startsWith(" ") ? <>  : ""; const last = s.endsWith(" ") ? <>  : ""; return ( -
+ <> {first} {s} {last} -
+ ); }; @@ -52,7 +52,7 @@ const SplitAndReplace: FC = ({ text, replacements }) => { if (parts.length === 0) { return <>{parts[0]}; } - return
{parts}
; + return <>{parts}; }; export default SplitAndReplace; From 3a393f6238b87fe87249f0a592154e8abdf52bfb Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 2 Jul 2020 11:29:17 +0200 Subject: [PATCH 13/15] unify name of ChangesetShortLink and export regex --- scm-ui/ui-components/src/MarkdownView.tsx | 2 +- ...nksParser.test.ts => remarkChangesetShortLinkParser.test.ts} | 2 +- ...rkCommitLinksParser.ts => remarkChangesetShortLinkParser.ts} | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) rename scm-ui/ui-components/src/{remarkCommitLinksParser.test.ts => remarkChangesetShortLinkParser.test.ts} (97%) rename scm-ui/ui-components/src/{remarkCommitLinksParser.ts => remarkChangesetShortLinkParser.ts} (99%) diff --git a/scm-ui/ui-components/src/MarkdownView.tsx b/scm-ui/ui-components/src/MarkdownView.tsx index e46c95370a..946fdc37c9 100644 --- a/scm-ui/ui-components/src/MarkdownView.tsx +++ b/scm-ui/ui-components/src/MarkdownView.tsx @@ -32,7 +32,7 @@ import MarkdownHeadingRenderer from "./MarkdownHeadingRenderer"; import { create } from "./MarkdownLinkRenderer"; import {useTranslation, WithTranslation, withTranslation} from "react-i18next"; import Notification from "./Notification"; -import { createTransformer } from "./remarkCommitLinksParser"; +import { createTransformer } from "./remarkChangesetShortLinkParser"; type Props = RouteComponentProps & WithTranslation & { content: string; diff --git a/scm-ui/ui-components/src/remarkCommitLinksParser.test.ts b/scm-ui/ui-components/src/remarkChangesetShortLinkParser.test.ts similarity index 97% rename from scm-ui/ui-components/src/remarkCommitLinksParser.test.ts rename to scm-ui/ui-components/src/remarkChangesetShortLinkParser.test.ts index a0af2aeee9..0e2211f3d0 100644 --- a/scm-ui/ui-components/src/remarkCommitLinksParser.test.ts +++ b/scm-ui/ui-components/src/remarkChangesetShortLinkParser.test.ts @@ -22,7 +22,7 @@ * SOFTWARE. */ -import { regExpPattern } from "./remarkCommitLinksParser"; +import { regExpPattern } from "./remarkChangesetShortLinkParser"; describe("Remark Commit Links RegEx Tests", () => { it("should match simple names", () => { diff --git a/scm-ui/ui-components/src/remarkCommitLinksParser.ts b/scm-ui/ui-components/src/remarkChangesetShortLinkParser.ts similarity index 99% rename from scm-ui/ui-components/src/remarkCommitLinksParser.ts rename to scm-ui/ui-components/src/remarkChangesetShortLinkParser.ts index fa26c793db..f9995e7131 100644 --- a/scm-ui/ui-components/src/remarkCommitLinksParser.ts +++ b/scm-ui/ui-components/src/remarkChangesetShortLinkParser.ts @@ -30,7 +30,6 @@ import { TFunction } from "i18next"; const namePartRegex = nameRegex.source.substring(1, nameRegex.source.length - 1); -// Visible for testing export const regExpPattern = `(${namePartRegex})\\/(${namePartRegex})@([\\w\\d]+)`; function match(value: string): RegExpMatchArray[] { @@ -45,7 +44,6 @@ function match(value: string): RegExpMatchArray[] { } export const createTransformer = (t: TFunction): MdastPlugin => { - return (tree: MarkdownAbstractSyntaxTree) => { visit(tree, "text", (node: MarkdownAbstractSyntaxTree, index: number, parent: MarkdownAbstractSyntaxTree) => { if (parent.type === "link" || !node.value) { From e08f2f22600d0b3f24568fb527c0947e91885cb1 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 2 Jul 2020 11:30:26 +0200 Subject: [PATCH 14/15] ensure both changeset short link matchers are using the same regex --- scm-ui/ui-components/src/index.ts | 1 + .../changesets/ChangesetShortLink.tsx | 35 ++++++++++--------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/scm-ui/ui-components/src/index.ts b/scm-ui/ui-components/src/index.ts index 9a8049a7bd..8fb3115c65 100644 --- a/scm-ui/ui-components/src/index.ts +++ b/scm-ui/ui-components/src/index.ts @@ -79,6 +79,7 @@ export { default as CardColumn } from "./CardColumn"; export { default as CardColumnSmall } from "./CardColumnSmall"; export { default as CommaSeparatedList } from "./CommaSeparatedList"; export { default as SplitAndReplace, Replacement } from "./SplitAndReplace"; +export { regExpPattern as changesetShortLinkRegex } from "./remarkChangesetShortLinkParser"; export { default as comparators } from "./comparators"; diff --git a/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetShortLink.tsx b/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetShortLink.tsx index e4f03a351f..6177dee5a9 100644 --- a/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetShortLink.tsx +++ b/scm-ui/ui-webapp/src/repos/components/changesets/ChangesetShortLink.tsx @@ -25,26 +25,27 @@ import React from "react"; import { Link } from "react-router-dom"; import { Changeset } from "@scm-manager/ui-types"; -import { Replacement } from "@scm-manager/ui-components"; +import { Replacement, changesetShortLinkRegex } from "@scm-manager/ui-components"; const ChangesetShortLink: (changeset: Changeset, value: string) => Replacement[] = (changeset, value) => { - const linkRegExp = "([A-Za-z0-9\.\-_]+)\/([A-Za-z0-9\.\-_]+)@([a-f0-9]+)"; - const matches = value.match(new RegExp(linkRegExp, "g")); - if (!matches) { - return []; + const regex = new RegExp(changesetShortLinkRegex, "g") + + const replacements: Replacement[] = []; + + let m = regex.exec(value); + while (m) { + const namespace = m[1]; + const name = m[2]; + const revision = m[3]; + const link = `/repo/${namespace}/${name}/code/changeset/${revision}`; + replacements.push({ + textToReplace: m[0], + replacement: {m[0]} + }); + m = regex.exec(value); } - return matches.map(value => { - const groups = value.match(new RegExp(linkRegExp)); - const namespace = groups[1]; - const name = groups[2]; - const revision = groups[3]; - const link = `/repo/${namespace}/${name}/changeset/${revision}`; - const replacement: Replacement = { - textToReplace: value, - replacement: {value} - }; - return replacement; - }); + + return replacements; }; export default ChangesetShortLink; From aabd5058d1a20eb6dfb132c857d74d8b7dd55204 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 2 Jul 2020 11:32:41 +0200 Subject: [PATCH 15/15] update storyshots --- .../src/__snapshots__/storyshots.test.ts.snap | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap index 8ac66dd2d9..1fe739ded1 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -1755,7 +1755,9 @@ exports[`Storyshots Changesets Co-Authors with avatar 1`] = `

+ Added design docs +

+ The starship Heart of Gold was the first spacecraft to make use of the Infinite Improbability Drive. The craft was stolen by then-President Zaphod Beeblebrox at the official launch of the ship, as he was supposed to be officiating the launch. Later, during the use of the Infinite Improbability Drive, the ship picked up Arthur Dent and Ford Prefect, who were floating unprotected in deep space in the same star sector, having just escaped the destruction of the same planet. +

+ initialize repository + + +

+

+

+ changeset.contributors.authoredBy + + + SCM Administrator + +

+ + + +
+
+ +
+
+

+ +

+

+ +

+
+
+ + + +`; + +exports[`Storyshots Changesets Replacements 1`] = ` +
+
+
+
+
+
+
+
+

+ + HOG-42 + +   + Change mail to +   + + Arthur +

+ Change heading to "Heart Of Gold" +

+ The starship Heart of Gold was the first spacecraft to make use of the Infinite Improbability Drive. The craft was stolen by then-President Zaphod Beeblebrox at the official launch of the ship, as he was supposed to be officiating the launch. Later, during the use of the Infinite Improbability Drive, the ship picked up Arthur Dent and Ford Prefect, who were floating unprotected in deep space in the same star sector, having just escaped the destruction of the same planet. +

+ initialize repository +

+ Added design docs +

`; +exports[`Storyshots SplitAndReplace Simple replacement 1`] = ` +Array [ +

+ + + So this is it, + + +   + said Arthur, +   + + + We are going to die. + + +
, +
+ + + Yes, + + +   + said Ford, +   + + + except... no! Wait a minute! + + +
, +] +`; + exports[`Storyshots SyntaxHighlighter Go 1`] = `