Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/2744/copy filenames to clipboard #2942

Merged
merged 42 commits into from Aug 22, 2022
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
d20b63a
Introduce new component for copying filenames to clipboard #2744
Aug 4, 2022
8848847
Add method for copying text to clipboard #2744
Aug 4, 2022
09fdd04
Add subscription of button to file state #2744
Aug 4, 2022
059d275
Save wip for file name extraction #2744
knoffi Aug 4, 2022
e54f375
test map mutating function #2744
knoffi Aug 8, 2022
4a18ad1
fix clipboard text construction (wip) #2744
knoffi Aug 8, 2022
8c3089f
refactor clipboard component (wip) #2744
knoffi Aug 9, 2022
ad4067b
Fix while loop in insertInFullDescendingList, plus adjust renaming, p…
Hall-Ma Aug 9, 2022
8445eff
test if clipboard button uses clipboard #2744
knoffi Aug 10, 2022
d40d17d
test if clipboard receives text from service #2744
knoffi Aug 10, 2022
751ec5e
fix test description of component #2744
knoffi Aug 10, 2022
43cf4b8
test clipboard service #2744
knoffi Aug 10, 2022
83cfcb5
test string building for clipboard #2744
knoffi Aug 10, 2022
1508ddc
test getFilenamesWithHighestMetrics #2744
knoffi Aug 10, 2022
3c767eb
rename function maybeAddToMap #2744
knoffi Aug 10, 2022
74198f3
Merge remote-tracking branch 'origin/main' into feature/2744/copy-fil…
knoffi Aug 10, 2022
2a6e6e7
document changelog #2744
knoffi Aug 10, 2022
17ed69c
Merge branch 'main' into feature/2744/copy-filenames-to-clipboard
knoffi Aug 11, 2022
4424803
document changes via picture #2744
knoffi Aug 11, 2022
61a68c2
refactor method for finding top 10 violators #2744
knoffi Aug 11, 2022
2cb75ee
document changelog with better text #2744
knoffi Aug 12, 2022
5ea8361
remove redundant test #2744
knoffi Aug 12, 2022
89ab875
refactor string construction for clipboard #2744
knoffi Aug 12, 2022
00ae527
refactor variable naming #2744
knoffi Aug 12, 2022
616f8d2
refactor function name #2744
knoffi Aug 12, 2022
58f7e91
Merge remote-tracking branch 'origin/main' into feature/2744/copy-fil…
knoffi Aug 15, 2022
4045391
remove comment and documentation #2744
knoffi Aug 15, 2022
448376b
rename text suite #2744
knoffi Aug 15, 2022
caf8910
refactor new lines after each it in test #2744
knoffi Aug 15, 2022
58c6583
fix left margin of copy-to-clipboard-button #2744
knoffi Aug 15, 2022
cc5c9ae
change button title of copy-to-clipboard #2744
knoffi Aug 16, 2022
3ed80be
rename test description #2744
knoffi Aug 16, 2022
c1c0263
Merge remote-tracking branch 'origin/main' into feature/2744/copy-fil…
knoffi Aug 16, 2022
7d386c5
refactor updateAttributeMap #2744
knoffi Aug 16, 2022
d73f0c3
change test description #2744
knoffi Aug 16, 2022
d246282
refactor free lines in test suite #2744
knoffi Aug 16, 2022
9aea723
describe test suite #2744
knoffi Aug 18, 2022
900536e
Merge remote-tracking branch 'origin/main' into feature/2744/copy-fil…
knoffi Aug 18, 2022
b2a8ada
test copyToClipBoardButton with tiny setup #2744
knoffi Aug 18, 2022
c885a91
Merge remote-tracking branch 'origin/main' into feature/2744/copy-fil…
knoffi Aug 18, 2022
af61e8e
test CopyToClipboardService with tiny setup #2744
knoffi Aug 18, 2022
c317429
Merge branch 'main' into feature/2744/copy-filenames-to-clipboard
knoffi Aug 19, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/)

## [unreleased] (Added 🚀 | Changed | Removed 🗑 | Fixed 🐞 | Chore 👨‍💻 👩‍💻)

### Added 🚀

- Copy-to-Clipboard-Button to paste the top 10 of files with highest metric values [#2942](https://github.com/MaibornWolff/codecharta/pull/2942)<br/>![image](https://user-images.githubusercontent.com/46388280/184089603-ecfa8e31-8241-42a2-9954-2de554347381.png)<br/>![image](https://user-images.githubusercontent.com/46388280/184089577-5cd2eec0-5293-4083-b629-0e3c5621047c.png)
knoffi marked this conversation as resolved.
Show resolved Hide resolved

## [1.103.3] - 2022-08-10

### Fixed 🐞
Expand Down
11 changes: 7 additions & 4 deletions visualization/app/app.module.ts
Expand Up @@ -18,11 +18,11 @@ import { UnfocusNodesOnLoadingMapEffect } from "./codeCharta/state/effects/unfoc
import { AddBlacklistItemsIfNotResultsInEmptyMapEffect } from "./codeCharta/state/effects/addBlacklistItemsIfNotResultsInEmptyMap/addBlacklistItemsIfNotResultsInEmptyMap.effect"
import { dialogs } from "./codeCharta/ui/dialogs/dialogs"
import {
threeSceneServiceProvider,
codeChartaServiceProvider,
threeOrbitControlsServiceProvider,
threeCameraServiceProvider,
threeRendererServiceProvider
threeOrbitControlsServiceProvider,
knoffi marked this conversation as resolved.
Show resolved Hide resolved
threeRendererServiceProvider,
threeSceneServiceProvider
} from "./codeCharta/services/ajs-upgraded-providers"
import { NodeContextMenuCardModule } from "./codeCharta/state/effects/nodeContextMenu/nodeContextMenuCard/nodeContextMenuCard.module"
import { OpenNodeContextMenuEffect } from "./codeCharta/state/effects/nodeContextMenu/openNodeContextMenu.effect"
Expand Down Expand Up @@ -58,6 +58,7 @@ import { ActionIconModule } from "./codeCharta/ui/actionIcon/actionIcon.module"
import { ColorSettingsPanelModule } from "./codeCharta/ui/ribbonBar/colorSettingsPanel/colorSettingsPanel.module"
import { ScreenshotButtonModule } from "./codeCharta/ui/screenshotButton/screenshotButton.module"
import { SplitStateActionsEffect } from "./codeCharta/state/effects/splitStateActionsEffect/splitStateActions.effect"
import { CopyToClipboardButtonModule } from "./codeCharta/ui/copyToClipboardButton/copyToClipboardButton.module"

@NgModule({
imports: [
Expand Down Expand Up @@ -99,7 +100,8 @@ import { SplitStateActionsEffect } from "./codeCharta/state/effects/splitStateAc
HoveredNodePathPanelModule,
ActionIconModule,
ColorSettingsPanelModule,
ScreenshotButtonModule
ScreenshotButtonModule,
CopyToClipboardButtonModule
],
providers: [
threeSceneServiceProvider,
Expand Down Expand Up @@ -132,6 +134,7 @@ import { SplitStateActionsEffect } from "./codeCharta/state/effects/splitStateAc
})
export class AppModule {
constructor(@Inject(UpgradeModule) private upgrade: UpgradeModule) {}

ngDoBootstrap() {
this.upgrade.bootstrap(document.body, ["app"], { strictDi: true })
}
Expand Down
@@ -0,0 +1,26 @@
import { TestBed } from "@angular/core/testing"
import { CopyToClipboardService } from "./copyToClipboard.service"
import { buildTextOfFiles } from "./util/clipboardString"
import { getFilenamesWithHighestMetrics } from "./util/getFilenamesWithHighestMetrics"
let service: CopyToClipboardService
knoffi marked this conversation as resolved.
Show resolved Hide resolved
jest.mock("./util/clipboardString", () => {
return { buildTextOfFiles: jest.fn().mockReturnValue("Magic Monday") }
})
jest.mock("./util/getFilenamesWithHighestMetrics", () => {
return { getFilenamesWithHighestMetrics: jest.fn() }
})
describe("copyToClipboardService", () => {
beforeEach(() => {
TestBed.configureTestingModule({ providers: [CopyToClipboardService] })
service = TestBed.inject(CopyToClipboardService)
knoffi marked this conversation as resolved.
Show resolved Hide resolved

jest.clearAllMocks()
})
it("should call text function and mapNode function on getClipboardText", () => {
knoffi marked this conversation as resolved.
Show resolved Hide resolved
const clipboardText = service.getClipboardText()

expect(clipboardText).toBe("Magic Monday")
expect(buildTextOfFiles).toHaveBeenCalled()
expect(getFilenamesWithHighestMetrics).toHaveBeenCalled()
})
})
@@ -0,0 +1,22 @@
import { Inject, Injectable } from "@angular/core"
import { State } from "../../state/angular-redux/state"
import { accumulatedDataSelector } from "../../state/selectors/accumulatedData/accumulatedData.selector"
import { buildTextOfFiles } from "./util/clipboardString"
import { getFilenamesWithHighestMetrics } from "./util/getFilenamesWithHighestMetrics"

@Injectable()
export class CopyToClipboardService {
constructor(@Inject(State) private state: State) {}

private getUnifiedMapNode() {
const { unifiedMapNode } = accumulatedDataSelector(this.state.getValue())
return unifiedMapNode
}

getClipboardText(): string {
const node = this.getUnifiedMapNode()
const filesByAttribute = getFilenamesWithHighestMetrics(node)

return buildTextOfFiles(filesByAttribute)
}
knoffi marked this conversation as resolved.
Show resolved Hide resolved
}
@@ -0,0 +1,2 @@
<cc-action-icon icon="fa fa-clipboard" title="Copy filenames with worst metric values to clipboard" (click)="copyNamesToClipBoard()">
knoffi marked this conversation as resolved.
Show resolved Hide resolved
</cc-action-icon>
@@ -0,0 +1,3 @@
cc-copy-to-clipboard-button {
knoffi marked this conversation as resolved.
Show resolved Hide resolved
margin-left: 8px;
}
@@ -0,0 +1,31 @@
import { TestBed } from "@angular/core/testing"
import { CopyToClipboardButtonComponent } from "./copyToClipboardButton.component"
import { CopyToClipboardButtonModule } from "./copyToClipboardButton.module"

describe("CopyToClipboardButtonComponent", () => {
let component: CopyToClipboardButtonComponent

beforeEach(() => {
const test = TestBed.configureTestingModule({
imports: [CopyToClipboardButtonModule]
})
component = test.createComponent<CopyToClipboardButtonComponent>(CopyToClipboardButtonComponent).componentInstance
component["service"].getClipboardText = () => "Magic Monday"
knoffi marked this conversation as resolved.
Show resolved Hide resolved

//clipboard does not exist in jest's JSdom, see https://stackoverflow.com/questions/62351935/how-to-mock-navigator-clipboard-writetext-in-jest
knoffi marked this conversation as resolved.
Show resolved Hide resolved
Object.assign(navigator, { clipboard: { writeText: jest.fn() } })
})

describe("copyToClipboardButtonComponent", () => {
it("should write text to clipboard by using text from service", async () => {
const clipboard = navigator.clipboard
const writeToClipboardSpy = jest.spyOn(clipboard, "writeText")
const getClipboardTextSpy = jest.spyOn(component["service"], "getClipboardText")

await component.copyNamesToClipBoard()

expect(writeToClipboardSpy).toBeCalledWith("Magic Monday")
expect(getClipboardTextSpy).toBeCalled()
knoffi marked this conversation as resolved.
Show resolved Hide resolved
})
})
})
@@ -0,0 +1,15 @@
import "./copyToClipboardButton.component.scss"
import { Component, Inject } from "@angular/core"
import { CopyToClipboardService } from "./copyToClipboard.service"

@Component({
selector: "cc-copy-to-clipboard-button",
template: require("./copyToClipboardButton.component.html")
})
export class CopyToClipboardButtonComponent {
constructor(@Inject(CopyToClipboardService) private service: CopyToClipboardService) {}
shaman-apprentice marked this conversation as resolved.
Show resolved Hide resolved

async copyNamesToClipBoard() {
await navigator.clipboard.writeText(this.service.getClipboardText())
}
}
@@ -0,0 +1,14 @@
import { CommonModule } from "@angular/common"
import { NgModule } from "@angular/core"
import { ActionIconModule } from "../actionIcon/actionIcon.module"
import { CopyToClipboardService } from "./copyToClipboard.service"
import { CopyToClipboardButtonComponent } from "./copyToClipboardButton.component"

@NgModule({
imports: [CommonModule, ActionIconModule],
declarations: [CopyToClipboardButtonComponent],
exports: [CopyToClipboardButtonComponent],
entryComponents: [CopyToClipboardButtonComponent],
providers: [CopyToClipboardService]
})
export class CopyToClipboardButtonModule {}
@@ -0,0 +1,42 @@
import { buildTextOfFiles } from "./clipboardString"
import { FileToValue } from "./getFilenamesWithHighestMetrics"

describe("buildTextOfFiles", () => {
it("should return only header line if there is only one attribute without files", () => {
const result = buildTextOfFiles(WITH_ONE_FILELESS_ATTRIBUTE)

expect(result).toBe("FUNCTIONS\n")
shaman-apprentice marked this conversation as resolved.
Show resolved Hide resolved
})
it("should return valid string if there two attributes", () => {
const result = buildTextOfFiles(WITH_TWO_ATTRIBUTES)

expect(result).toBe(
`RLOC\n` + `\t${String.fromCodePoint(8226)} fileA (12)\n` + `COMMENTS\n` + `\t${String.fromCodePoint(8226)} fileA (14)\n`
knoffi marked this conversation as resolved.
Show resolved Hide resolved
)
})
it("should return valid string if there is one attribute with many files", () => {
knoffi marked this conversation as resolved.
Show resolved Hide resolved
const result = buildTextOfFiles(WITH_ONE_ATTRIBUTE)

expect(result).toBe(
`MCC\n` +
`\t${String.fromCodePoint(8226)} file1 (100)\n` +
`\t${String.fromCodePoint(8226)} file2 (84)\n` +
`\t${String.fromCodePoint(8226)} file3 (122)\n`
)
})
})
const WITH_ONE_ATTRIBUTE = new Map<string, FileToValue[]>([
[
"mcc",
[
{ name: "file1", value: 100 },
{ name: "file2", value: 84 },
{ name: "file3", value: 122 }
]
]
])
const WITH_TWO_ATTRIBUTES = new Map<string, FileToValue[]>([
["rloc", [{ name: "fileA", value: 12 }]],
["comments", [{ name: "fileA", value: 14 }]]
])
const WITH_ONE_FILELESS_ATTRIBUTE = new Map<string, FileToValue[]>([["functions", []]])
knoffi marked this conversation as resolved.
Show resolved Hide resolved
@@ -0,0 +1,21 @@
import { FileToValue } from "./getFilenamesWithHighestMetrics"

function getStringLineFromItem(item: FileToValue): string {
knoffi marked this conversation as resolved.
Show resolved Hide resolved
return `\t${String.fromCodePoint(8226)} ${item.name} (${item.value})` + `\n`
knoffi marked this conversation as resolved.
Show resolved Hide resolved
}

function getStringHeaderFromAttribute(title: string): string {
return `${title.toUpperCase()}\n`
}

export function buildTextOfFiles(attributeToFiles: Map<string, FileToValue[]>): string {
let clipboardText = ""
knoffi marked this conversation as resolved.
Show resolved Hide resolved
for (const [key, files] of attributeToFiles.entries()) {
clipboardText += getStringHeaderFromAttribute(key)
for (const file of files) {
clipboardText += getStringLineFromItem(file)
}
}

return clipboardText
}
@@ -0,0 +1,137 @@
import { NodeType } from "../../../codeCharta.model"
import { FileToValue, getFilenamesWithHighestMetrics, updateAttributeMap } from "./getFilenamesWithHighestMetrics"

let MAP: Map<string, FileToValue[]>
knoffi marked this conversation as resolved.
Show resolved Hide resolved

beforeEach(() => {
MAP = new Map<string, FileToValue[]>([
[
"mcc",
[
{ name: "file2", value: 500 },
{ name: "file3", value: 400 },
{ name: "file1", value: 300 },
{ name: "file1", value: 200 },
{ name: "file1", value: 100 },
{ name: "file1", value: 50 },
{ name: "file1", value: 20 },
{ name: "file1", value: 15 },
{ name: "file1", value: 10 },
{ name: "file1", value: 5 }
]
]
])
})

describe("getFilenamesWithHighestMetrics", () => {
it("should give map which has exactly two keys (mcc, rloc)", () => {
knoffi marked this conversation as resolved.
Show resolved Hide resolved
const resultMap = getFilenamesWithHighestMetrics(BIG_NODE_WITH_TWO_ATTRIBUTES)
const mccResults = resultMap.get("mcc")
const rlocResults = resultMap.get("rloc")

expect(resultMap.size).toBe(2)
expect(mccResults).toBeTruthy()
expect(rlocResults).toBeTruthy()
})
knoffi marked this conversation as resolved.
Show resolved Hide resolved
it("should restrict to at most 10 files for each attribute", () => {
const resultMap = getFilenamesWithHighestMetrics(BIG_NODE_WITH_TWO_ATTRIBUTES)

for (const files of resultMap) {
expect(files.length).toBeLessThanOrEqual(10)
}
})
it("should return correct values for rloc and mcc", () => {
knoffi marked this conversation as resolved.
Show resolved Hide resolved
const resultMap = getFilenamesWithHighestMetrics(BIG_NODE_WITH_TWO_ATTRIBUTES)
const mccResults = resultMap.get("mcc")
const rlocResults = resultMap.get("rloc")

expect(rlocResults).toEqual([{ name: "leaf1", value: 5 }])
expect(mccResults).toEqual([
{ name: "leaf1", value: 10 },
{ name: "leaf2", value: 9 },
{ name: "leaf3", value: 8 },
{ name: "leaf4", value: 7 },
{ name: "leaf5", value: 6 },
{ name: "leaf6", value: 5 },
{ name: "leaf7", value: 4 },
{ name: "leaf8", value: 3 },
{ name: "leaf9", value: 2 },
{ name: "leaf10", value: 1 }
])
})
it("should ignore folders", () => {
const map = getFilenamesWithHighestMetrics(CONTAINS_FOLDER_WITH_ATTRIBUTES)

expect(map.get("rloc")).toBeUndefined()
expect(map.get("mcc")).toEqual([{ name: "file", value: 0 }])
})
})

describe("updateAttributeMap", () => {
it("should not add if value is too low", () => {
shaman-apprentice marked this conversation as resolved.
Show resolved Hide resolved
updateAttributeMap("mcc", 0, "file with low value", MAP)

expect(MAP.get("mcc")).not.toContainEqual({ name: "file with low value", value: 0 })
expect(MAP.get("mcc").length).toBe(10)
})
it("should add if value is high", () => {
knoffi marked this conversation as resolved.
Show resolved Hide resolved
updateAttributeMap("mcc", 9001, "file with highest value", MAP)

expect(MAP.get("mcc")).toContainEqual({ name: "file with highest value", value: 9001 })
expect(MAP.get("mcc").length).toBe(10)
})
it("should add if map is empty", () => {
knoffi marked this conversation as resolved.
Show resolved Hide resolved
const EMPTY_MAP = new Map<string, FileToValue[]>()

updateAttributeMap("mcc", 0, "first file", EMPTY_MAP)

expect(EMPTY_MAP.get("mcc")).toContainEqual({ name: "first file", value: 0 })
expect(EMPTY_MAP.get("mcc").length).toBe(1)
})
it("should add if key has less than 10 values", () => {
knoffi marked this conversation as resolved.
Show resolved Hide resolved
const TINY_MAP = new Map<string, FileToValue[]>([["mcc", [{ name: "first file", value: 424_242 }]]])

updateAttributeMap("mcc", 0, "second file", TINY_MAP)

expect(TINY_MAP.get("mcc")).toContainEqual({ name: "second file", value: 0 })
expect(TINY_MAP.get("mcc").length).toBe(2)
})
it("should add if key is different", () => {
knoffi marked this conversation as resolved.
Show resolved Hide resolved
updateAttributeMap("rloc", 0, "first rloc file", MAP)

expect(MAP.get("rloc")).toEqual([{ name: "first rloc file", value: 0 }])
expect(MAP.get("mcc").length).toBe(10)
expect(MAP.size).toBe(2)
})
})

const BIG_NODE_WITH_TWO_ATTRIBUTES = {
name: "root",
type: NodeType.FOLDER,
children: [
{
name: "folder",
type: NodeType.FOLDER,
children: [
{ name: "leaf2", attributes: { ["mcc"]: 9 }, type: NodeType.FILE },
{ name: "leaf3", attributes: { ["mcc"]: 8 }, type: NodeType.FILE },
{ name: "leaf4", attributes: { ["mcc"]: 7 }, type: NodeType.FILE },
{ name: "leaf5", attributes: { ["mcc"]: 6 }, type: NodeType.FILE },
{ name: "leaf6", attributes: { ["mcc"]: 5 }, type: NodeType.FILE },
{ name: "leaf7", attributes: { ["mcc"]: 4 }, type: NodeType.FILE },
{ name: "leaf8", attributes: { ["mcc"]: 3 }, type: NodeType.FILE },
{ name: "leaf9", attributes: { ["mcc"]: 2 }, type: NodeType.FILE },
{ name: "leaf10", attributes: { ["mcc"]: 1 }, type: NodeType.FILE },
{ name: "leaf11", attributes: { ["mcc"]: 0 }, type: NodeType.FILE }
]
},
{ name: "leaf1", attributes: { ["mcc"]: 10, ["rloc"]: 5 }, type: NodeType.FILE }
]
}

const CONTAINS_FOLDER_WITH_ATTRIBUTES = {
name: "root_folder",
type: NodeType.FOLDER,
attributes: { mcc: 111, rloc: 15 },
children: [{ name: "file", type: NodeType.FILE, attributes: { mcc: 0 } }]
}
knoffi marked this conversation as resolved.
Show resolved Hide resolved