diff --git a/visualization/CHANGELOG.md b/visualization/CHANGELOG.md index d8b2838dd0..ac64a9f915 100644 --- a/visualization/CHANGELOG.md +++ b/visualization/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/) ### Added - Add a cross-hair when hovering over the color quantile diagram [#3827](https://github.com/MaibornWolff/codecharta/pull/3827) +- Maps are always shown and rendered in alphabetical order [#3905](https://github.com/MaibornWolff/codecharta/pull/3905) ### Changed diff --git a/visualization/app/codeCharta/e2e/url.e2e.ts b/visualization/app/codeCharta/e2e/url.e2e.ts index b35ddc3302..0e853c7725 100644 --- a/visualization/app/codeCharta/e2e/url.e2e.ts +++ b/visualization/app/codeCharta/e2e/url.e2e.ts @@ -78,7 +78,7 @@ describe("codecharta", () => { it("should load data when file parameters in url are valid", async () => { await mockResponses() await goto(`${CC_URL}?file=fileOne.json&file=fileTwo.json`) - await checkAllFileNames(["Sample Project with Edges", "Sample Project"]) + await checkAllFileNames(["Sample Project", "Sample Project with Edges"]) }) it("should throw errors when file parameters in url are invalid and load sample data instead", async () => { diff --git a/visualization/app/codeCharta/services/loadFile/loadFile.service.spec.ts b/visualization/app/codeCharta/services/loadFile/loadFile.service.spec.ts index 6b571292df..adc5cb3988 100644 --- a/visualization/app/codeCharta/services/loadFile/loadFile.service.spec.ts +++ b/visualization/app/codeCharta/services/loadFile/loadFile.service.spec.ts @@ -229,12 +229,14 @@ describe("loadFileService", () => { expect(storeDispatchSpy).toHaveBeenCalledWith(setCurrentFilesAreSampleFiles({ value: false })) expect(CCFilesUnderTest.length).toEqual(3) - expect(CCFilesUnderTest[0].fileMeta.fileName).toEqual("SecondFile") - expect(CCFilesUnderTest[0].fileMeta.fileChecksum).toEqual("hash_1") - expect(CCFilesUnderTest[1].fileMeta.fileName).toEqual("ThirdFile") - expect(CCFilesUnderTest[1].fileMeta.fileChecksum).toEqual("hash_2_1") - expect(CCFilesUnderTest[2].fileMeta.fileName).toEqual("FourthFile") - expect(CCFilesUnderTest[2].fileMeta.fileChecksum).toEqual("hash_3") + const allFileNames = CCFilesUnderTest.map(file => file.fileMeta.fileName) + expect(allFileNames).toContain("SecondFile") + expect(allFileNames).toContain("ThirdFile") + expect(allFileNames).toContain("FourthFile") + const allChecksums = CCFilesUnderTest.map(file => file.fileMeta.fileChecksum) + expect(allChecksums).toContain("hash_1") + expect(allChecksums).toContain("hash_2_1") + expect(allChecksums).toContain("hash_3") }) it("should keep sample files when loading the same sample file and after that another different file", () => { @@ -352,7 +354,7 @@ describe("loadFileService", () => { try { codeChartaService.loadFiles([{ fileName: "DifferentName", content: validFileContent, fileSize: 42 }]) - } catch (e) {} + } catch (_) {} const CCFilesUnderTest = getCCFiles(state.getValue().files) @@ -370,7 +372,7 @@ describe("loadFileService", () => { try { codeChartaService.loadFiles([{ fileName: "FirstFile", content: validFileContent, fileSize: 42 }]) - } catch (e) {} + } catch (_) {} const filesUnderTest: FileState[] = state.getValue().files @@ -602,8 +604,7 @@ describe("loadFileService", () => { }) it("should load files ignoring the authors attribute", () => { - const testFileContentWithAuthors = TEST_FILE_CONTENT_WITH_AUTHORS - const testJsonWithAuthors = JSON.stringify(testFileContentWithAuthors) + const testJsonWithAuthors = JSON.stringify(TEST_FILE_CONTENT_WITH_AUTHORS) const expectedFileContentWithoutAuthors = TEST_FILE_CONTENT_WITHOUT_AUTHORS const ccFile = getCCFileAndDecorateFileChecksum(testJsonWithAuthors) @@ -613,8 +614,7 @@ describe("loadFileService", () => { }) it("should show warnings for files containing the authors attribute", () => { - const testFileContentWithAuthors = TEST_FILE_CONTENT_WITH_AUTHORS - const testJsonWithAuthors = JSON.stringify(testFileContentWithAuthors) + const testJsonWithAuthors = JSON.stringify(TEST_FILE_CONTENT_WITH_AUTHORS) const expectedFileValidationResult: CCFileValidationResult[] = [ { fileName: "FirstFile", diff --git a/visualization/app/codeCharta/state/store/files/files.reducer.spec.ts b/visualization/app/codeCharta/state/store/files/files.reducer.spec.ts index efa6b98cb5..01afd99414 100644 --- a/visualization/app/codeCharta/state/store/files/files.reducer.spec.ts +++ b/visualization/app/codeCharta/state/store/files/files.reducer.spec.ts @@ -9,7 +9,7 @@ import { setStandardByNames, switchReferenceAndComparison } from "./files.actions" -import { TEST_DELTA_MAP_A, TEST_DELTA_MAP_B } from "../../../util/dataMocks" +import { TEST_DELTA_MAP_A, TEST_DELTA_MAP_B, TEST_FILE_DATA } from "../../../util/dataMocks" import { isDeltaState, isPartialState } from "../../../model/files/files.helper" import { FileSelectionState, FileState } from "../../../model/files/files" import { clone } from "../../../util/clone" @@ -31,6 +31,65 @@ describe("files", () => { expect(result).toEqual(newFiles) }) + + it("should set new files and sort them in descending order by name and checksum", () => { + const newFiles: FileState[] = [ + { + file: { ...TEST_FILE_DATA, fileMeta: { ...TEST_FILE_DATA.fileMeta, fileName: "B", fileChecksum: "1" } }, + selectedAs: FileSelectionState.Partial + }, + { + file: { ...TEST_FILE_DATA, fileMeta: { ...TEST_FILE_DATA.fileMeta, fileName: "A", fileChecksum: "2" } }, + selectedAs: FileSelectionState.Partial + }, + { + file: { ...TEST_FILE_DATA, fileMeta: { ...TEST_FILE_DATA.fileMeta, fileName: "C", fileChecksum: "3" } }, + selectedAs: FileSelectionState.Partial + }, + { + file: { ...TEST_FILE_DATA, fileMeta: { ...TEST_FILE_DATA.fileMeta, fileName: "B", fileChecksum: "4" } }, + selectedAs: FileSelectionState.Partial + } + ] + + const result = files(state, setFiles({ value: newFiles })) + + expect(result.length).toBe(4) + expect(result[0].file.fileMeta.fileName).toBe("A") + expect(result[0].file.fileMeta.fileChecksum).toBe("2") + expect(result[1].file.fileMeta.fileName).toBe("B") + expect(result[1].file.fileMeta.fileChecksum).toBe("1") + expect(result[2].file.fileMeta.fileName).toBe("B") + expect(result[2].file.fileMeta.fileChecksum).toBe("4") + expect(result[3].file.fileMeta.fileName).toBe("C") + expect(result[3].file.fileMeta.fileChecksum).toBe("3") + }) + }) + + describe("Action: ADD_FILE", () => { + it("should add a file and sort the files in descending order by name and checksum", () => { + const newFile1 = { ...TEST_FILE_DATA, fileMeta: { ...TEST_FILE_DATA.fileMeta, fileName: "B", fileChecksum: "1" } } + const newFile2 = { ...TEST_FILE_DATA, fileMeta: { ...TEST_FILE_DATA.fileMeta, fileName: "A", fileChecksum: "2" } } + const newFile3 = { ...TEST_FILE_DATA, fileMeta: { ...TEST_FILE_DATA.fileMeta, fileName: "C", fileChecksum: "3" } } + const newFile4 = { ...TEST_FILE_DATA, fileMeta: { ...TEST_FILE_DATA.fileMeta, fileName: "B", fileChecksum: "4" } } + + state = files(state, addFile({ file: newFile1 })) + state = files(state, addFile({ file: newFile2 })) + state = files(state, addFile({ file: newFile3 })) + const result = files(state, addFile({ file: newFile4 })) + + expect(result.length).toBe(6) + expect(result[0].file.fileMeta.fileName).toBe("A") + expect(result[0].file.fileMeta.fileChecksum).toBe("2") + expect(result[1].file.fileMeta.fileName).toBe("B") + expect(result[1].file.fileMeta.fileChecksum).toBe("1") + expect(result[2].file.fileMeta.fileName).toBe("B") + expect(result[2].file.fileMeta.fileChecksum).toBe("4") + expect(result[3].file.fileMeta.fileName).toBe("C") + expect(result[3].file.fileMeta.fileChecksum).toBe("3") + expect(result[4].file.fileMeta.fileName).toBe("fileA") + expect(result[5].file.fileMeta.fileName).toBe("fileB") + }) }) describe("Action: SET_DELTA", () => { diff --git a/visualization/app/codeCharta/state/store/files/files.reducer.ts b/visualization/app/codeCharta/state/store/files/files.reducer.ts index c75e36bfa6..0d1a798e2f 100644 --- a/visualization/app/codeCharta/state/store/files/files.reducer.ts +++ b/visualization/app/codeCharta/state/store/files/files.reducer.ts @@ -13,13 +13,12 @@ import { CCFile } from "../../../codeCharta.model" import { FileSelectionState, FileState } from "../../../model/files/files" import { isEqual } from "../../../model/files/files.helper" import { createReducer, on } from "@ngrx/store" -import { setState } from "../util/setState.reducer.factory" export const defaultFiles: FileState[] = [] export const files = createReducer( defaultFiles, - on(setFiles, setState(defaultFiles)), - on(addFile, (state, action) => [...state, { file: action.file, selectedAs: FileSelectionState.None }]), + on(setFiles, (state, action) => setFileStatesState(state, action.value)), + on(addFile, (state, action) => insertIntoSortedState(state, action.file)), on(removeFiles, (state, action) => removeFilesFromState(state, action.fileNames)), on(setDelta, (state, action) => setDeltaState(state, action.referenceFile, action.comparisonFile)), on(setDeltaReference, (state, action) => setDeltaReferenceState(state, action.file)), @@ -34,6 +33,32 @@ export const files = createReducer( on(setStandardByNames, (state, action) => setStandardByNamesState(state, action.fileNames)) ) +function setFileStatesState(_: FileState[], fileStates: FileState[]): FileState[] { + if (fileStates === undefined) { + return defaultFiles + } + return [...fileStates].sort((fileStateA, fileStateB) => (greaterEqualThan(fileStateA.file, fileStateB.file) ? 1 : -1)) +} + +function insertIntoSortedState(state: FileState[], file: CCFile) { + const index = state.findIndex(fileState => greaterEqualThan(fileState.file, file)) + const fileState = { file, selectedAs: FileSelectionState.None } + if (index === -1) { + return [...state, fileState] + } + return [...state.slice(0, index), fileState, ...state.slice(index)] +} + +function greaterEqualThan(fileA: CCFile, fileB: CCFile): boolean { + if (fileA.fileMeta.fileName > fileB.fileMeta.fileName) { + return true + } + if (fileA.fileMeta.fileName < fileB.fileMeta.fileName) { + return false + } + return fileA.fileMeta.fileChecksum >= fileB.fileMeta.fileChecksum +} + function removeFilesFromState(state: FileState[], fileNames: string[]): FileState[] { if (fileNames.length === 0) { return state diff --git a/visualization/app/codeCharta/ui/filePanel/fileSelectionMode.service.spec.ts b/visualization/app/codeCharta/ui/filePanel/fileSelectionMode.service.spec.ts index b5df7575a3..5847694522 100644 --- a/visualization/app/codeCharta/ui/filePanel/fileSelectionMode.service.spec.ts +++ b/visualization/app/codeCharta/ui/filePanel/fileSelectionMode.service.spec.ts @@ -19,9 +19,9 @@ describe("FileSelectionModeService", () => { }) store = TestBed.inject(Store) state = TestBed.inject(State) + store.dispatch(addFile({ file: TEST_FILE_DATA_JAVA })) //fileName: "fileB" store.dispatch(addFile({ file: TEST_FILE_DATA })) - store.dispatch(addFile({ file: TEST_FILE_DATA_JAVA })) - store.dispatch(setStandard({ files: [TEST_FILE_DATA] })) + store.dispatch(setStandard({ files: [TEST_FILE_DATA] })) //fileName: "fileA" fileSelectionModeService = new FileSelectionModeService(store, state) }) @@ -38,8 +38,8 @@ describe("FileSelectionModeService", () => { fileSelectionModeService.toggle() store.dispatch(setDelta({ referenceFile: TEST_FILE_DATA_JAVA, comparisonFile: TEST_FILE_DATA })) - fileSelectionModeService.toggle() + fileStates = state.getValue().files expect(fileStates[0].selectedAs).toBe(FileSelectionState.Partial) expect(fileStates[1].selectedAs).toBe(FileSelectionState.None) @@ -53,4 +53,26 @@ describe("FileSelectionModeService", () => { fileSelectionModeService.toggle() expect(referenceFileSelector(state.getValue())).toBe(TEST_FILE_DATA) }) + + it("should remain sorted after toggling", () => { + const file1 = { ...TEST_FILE_DATA, fileMeta: { ...TEST_FILE_DATA.fileMeta, fileName: "bFile", fileChecksum: "2" } } + const file2 = { ...TEST_FILE_DATA_JAVA, fileMeta: { ...TEST_FILE_DATA_JAVA.fileMeta, fileName: "aFile", fileChecksum: "1" } } + const file3 = { ...TEST_FILE_DATA, fileMeta: { ...TEST_FILE_DATA.fileMeta, fileName: "aFile", fileChecksum: "2" } } + + store.dispatch(addFile({ file: file1 })) + store.dispatch(addFile({ file: file2 })) + store.dispatch(addFile({ file: file3 })) + + fileSelectionModeService.toggle() + fileSelectionModeService.toggle() + + const fileStates = state.getValue().files + expect(fileStates[0].file.fileMeta.fileName).toBe("aFile") + expect(fileStates[0].file.fileMeta.fileChecksum).toBe("1") + expect(fileStates[1].file.fileMeta.fileName).toBe("aFile") + expect(fileStates[1].file.fileMeta.fileChecksum).toBe("2") + expect(fileStates[2].file.fileMeta.fileName).toBe("bFile") + expect(fileStates[3].file.fileMeta.fileName).toBe("fileA") + expect(fileStates[4].file.fileMeta.fileName).toBe("fileB") + }) }) diff --git a/visualization/app/codeCharta/ui/toolBar/uploadFilesButton/uploadFiles.service.spec.ts b/visualization/app/codeCharta/ui/toolBar/uploadFilesButton/uploadFiles.service.spec.ts index 306b6b598b..20a6da65b8 100644 --- a/visualization/app/codeCharta/ui/toolBar/uploadFilesButton/uploadFiles.service.spec.ts +++ b/visualization/app/codeCharta/ui/toolBar/uploadFilesButton/uploadFiles.service.spec.ts @@ -33,7 +33,7 @@ describe("UploadFilesService", () => { mockFileInput = { files: [new File([stringify(TEST_FILE_CONTENT)], "test.cc.json", { type: "application/json" })], click: jest.fn(), - addEventListener: jest.fn((event, callback) => {}) + addEventListener: jest.fn((_event, _callback) => {}) } as unknown as HTMLInputElement ;(createCCFileInput as jest.Mock).mockReturnValue(mockFileInput) })