From 00cf40d429b46c9b9568b47bcc3d7d8adc4d5c9f Mon Sep 17 00:00:00 2001 From: jq1836 <95712150+jq1836@users.noreply.github.com> Date: Sat, 28 Oct 2023 20:59:47 +0800 Subject: [PATCH 1/4] [#2054] Fix zoom view bug (#2055) Currently, when granularity is set to day or week, clicking on a ramp will open up a zoom view where commit messages are not being displayed and sorting by insertions does not result in any sorting. Let's fix the unintended behaviour of the zoom view. --- frontend/src/components/c-ramp.vue | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/c-ramp.vue b/frontend/src/components/c-ramp.vue index 5ca70a7b81..7440e6d2e1 100644 --- a/frontend/src/components/c-ramp.vue +++ b/frontend/src/components/c-ramp.vue @@ -142,8 +142,15 @@ export default defineComponent({ } const zoomUser = { ...user }; - // Type cast here is unsafe - zoomUser.commits = user.dailyCommits as Commit[]; + // Calculate total commit result insertion and deletion for the daily/weekly commit selected + zoomUser.commits = user.dailyCommits.map( + (dailyCommit) => ({ + insertions: dailyCommit.commitResults.reduce((acc, currCommitResult) => acc + currCommitResult.insertions, 0), + deletions: dailyCommit.commitResults.reduce((acc, currCommitResult) => acc + currCommitResult.deletions, 0), + ...dailyCommit, + commitResults: dailyCommit.commitResults.map((commitResult) => ({ ...commitResult, isOpen: true })), + }), + ) as Commit[]; const info = { zRepo: user.repoName, From 4dae85d654bb3ad433e0b18bac3d2f87a327ea88 Mon Sep 17 00:00:00 2001 From: jq1836 <95712150+jq1836@users.noreply.github.com> Date: Sat, 28 Oct 2023 21:16:40 +0800 Subject: [PATCH 2/4] [#1936] Migrate repo-sorter.js to typescript (#2052) Currently, there is still some JavaScript code which remains unmigrated. This allows for type unsafe code to be written, potentially resulting in unintended behavior. Let's migrate repo-sorter.js to TypeScript code to facilitate future changes to the code. --- frontend/src/utils/repo-sorter.js | 125 -------------------- frontend/src/utils/repo-sorter.ts | 182 ++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+), 125 deletions(-) delete mode 100644 frontend/src/utils/repo-sorter.js create mode 100644 frontend/src/utils/repo-sorter.ts diff --git a/frontend/src/utils/repo-sorter.js b/frontend/src/utils/repo-sorter.js deleted file mode 100644 index 673fc7d509..0000000000 --- a/frontend/src/utils/repo-sorter.js +++ /dev/null @@ -1,125 +0,0 @@ -function getTotalCommits(total, group) { - return total + group.checkedFileTypeContribution; -} - -function getGroupCommitsVariance(total, group) { - return total + group.variance; -} - -function sortingHelper(element, sortingOption) { - if (sortingOption === 'totalCommits') { - return element.reduce(getTotalCommits, 0); - } - if (sortingOption === 'variance') { - return element.reduce(getGroupCommitsVariance, 0); - } - if (sortingOption === 'displayName') { - return window.getAuthorDisplayName(element); - } - return element[0][sortingOption]; -} - -function groupByRepos(repos, sortingControl) { - const sortedRepos = []; - const { - sortingWithinOption, sortingOption, isSortingDsc, isSortingWithinDsc, - } = sortingControl; - const sortWithinOption = sortingWithinOption === 'title' ? 'displayName' : sortingWithinOption; - const sortOption = sortingOption === 'groupTitle' ? 'searchPath' : sortingOption; - repos.forEach((users) => { - if (sortWithinOption === 'totalCommits') { - users.sort(window.comparator((ele) => ele.checkedFileTypeContribution)); - } else { - users.sort(window.comparator((ele) => ele[sortWithinOption])); - } - - if (isSortingWithinDsc) { - users.reverse(); - } - sortedRepos.push(users); - }); - sortedRepos.sort(window.comparator(sortingHelper, sortOption)); - if (isSortingDsc) { - sortedRepos.reverse(); - } - return sortedRepos; -} - -function groupByNone(repos, sortingControl) { - const sortedRepos = []; - const { sortingOption, isSortingDsc } = sortingControl; - const isSortingGroupTitle = sortingOption === 'groupTitle'; - repos.forEach((users) => { - users.forEach((user) => { - sortedRepos.push(user); - }); - }); - sortedRepos.sort(window.comparator((repo) => { - if (isSortingGroupTitle) { - return `${repo.searchPath}${repo.name}`; - } - if (sortingOption === 'totalCommits') { - return repo.checkedFileTypeContribution; - } - return repo[sortingOption]; - })); - if (isSortingDsc) { - sortedRepos.reverse(); - } - - return sortedRepos; -} - -function groupByAuthors(repos, sortingControl) { - const authorMap = {}; - const filtered = []; - const { - sortingWithinOption, sortingOption, isSortingDsc, isSortingWithinDsc, - } = sortingControl; - const sortWithinOption = sortingWithinOption === 'title' ? 'searchPath' : sortingWithinOption; - const sortOption = sortingOption === 'groupTitle' ? 'displayName' : sortingOption; - repos.forEach((users) => { - users.forEach((user) => { - if (Object.keys(authorMap).includes(user.name)) { - authorMap[user.name].push(user); - } else { - authorMap[user.name] = [user]; - } - }); - }); - Object.keys(authorMap).forEach((author) => { - if (sortWithinOption === 'totalCommits') { - authorMap[author].sort(window.comparator((repo) => repo.checkedFileTypeContribution)); - } else { - authorMap[author].sort(window.comparator((repo) => repo[sortWithinOption])); - } - if (isSortingWithinDsc) { - authorMap[author].reverse(); - } - filtered.push(authorMap[author]); - }); - - filtered.sort(window.comparator(sortingHelper, sortOption)); - if (isSortingDsc) { - filtered.reverse(); - } - return filtered; -} - -function sortFiltered(filtered, filterControl) { - const { filterGroupSelection } = filterControl; - let full = []; - - if (filterGroupSelection === 'groupByNone') { - // push all repos into the same group - full[0] = groupByNone(filtered, filterControl); - } else if (filterGroupSelection === 'groupByAuthors') { - full = groupByAuthors(filtered, filterControl); - } else { - full = groupByRepos(filtered, filterControl); - } - - return full; -} - -export default sortFiltered; diff --git a/frontend/src/utils/repo-sorter.ts b/frontend/src/utils/repo-sorter.ts new file mode 100644 index 0000000000..7c1fd560d8 --- /dev/null +++ b/frontend/src/utils/repo-sorter.ts @@ -0,0 +1,182 @@ +import { User } from '../types/types'; +import { FilterGroupSelection } from '../types/summary'; + +function getTotalCommits(total: number, group: User): number { + // If group.checkedFileTypeContribution === undefined, then we treat it as 0 contribution + return total + (group.checkedFileTypeContribution ?? 0); +} + +function getGroupCommitsVariance(total: number, group: User): number { + return total + group.variance; +} + +function checkKeyAndGetValue(element: T, key?: string) { + // invalid key provided + if (key === undefined || !(key in element)) { + return undefined; + } + return element[key as keyof T]; +} + +/** + * Returns an empty string if an invalid/no key is provided or if the value retrieved is not a ComparablePrimitive. + * This permits and results in no sorting being done in the above cases. If function is to be made stricter, assertions + * or errors may be thrown where empty strings are returned. + */ +function getComparablePrimitive(element: User, key?: string): string | number { + const val = checkKeyAndGetValue(element, key); + // value retrieved is not a comparable primitive + if (typeof val !== 'string' && typeof val !== 'number') { + return ''; + } + return val; +} + +/** + * Array is not sorted when sortingOption is not provided. sortingOption is optional to allow it to fit the + * SortingFunction interface. + * */ +function sortingHelper(element: User[], sortingOption?: string): string | number { + switch (sortingOption) { + case 'totalCommits': + return element.reduce(getTotalCommits, 0); + case 'variance': + return element.reduce(getGroupCommitsVariance, 0); + case 'displayName': + return window.getAuthorDisplayName(element); + default: + return getComparablePrimitive(element[0], sortingOption); + } +} + +function groupByRepos( + repos: User[][], + sortingControl: { + sortingOption: string; + sortingWithinOption: string; + isSortingDsc: string; + isSortingWithinDsc: string; }, +): User[][] { + const sortedRepos: User[][] = []; + const { + sortingWithinOption, sortingOption, isSortingDsc, isSortingWithinDsc, + } = sortingControl; + const sortWithinOption = sortingWithinOption === 'title' ? 'displayName' : sortingWithinOption; + const sortOption = sortingOption === 'groupTitle' ? 'searchPath' : sortingOption; + repos.forEach((users) => { + if (sortWithinOption === 'totalCommits') { + users.sort(window.comparator((ele) => ele.checkedFileTypeContribution ?? 0)); + } else { + users.sort(window.comparator((ele) => getComparablePrimitive(ele, sortWithinOption))); + } + + if (isSortingWithinDsc) { + users.reverse(); + } + sortedRepos.push(users); + }); + sortedRepos.sort(window.comparator(sortingHelper, sortOption)); + if (isSortingDsc) { + sortedRepos.reverse(); + } + return sortedRepos; +} + +function groupByNone( + repos: User[][], + sortingControl: { + sortingOption: string; + isSortingDsc: string; }, +): User[] { + const sortedRepos: User[] = []; + const { sortingOption, isSortingDsc } = sortingControl; + const isSortingGroupTitle = sortingOption === 'groupTitle'; + repos.forEach((users) => { + users.forEach((user) => { + sortedRepos.push(user); + }); + }); + sortedRepos.sort(window.comparator((repo) => { + if (isSortingGroupTitle) { + return `${repo.searchPath}${repo.name}`; + } + if (sortingOption === 'totalCommits') { + return repo.checkedFileTypeContribution ?? 0; + } + return getComparablePrimitive(repo, sortingOption); + })); + if (isSortingDsc) { + sortedRepos.reverse(); + } + + return sortedRepos; +} + +function groupByAuthors( + repos: User[][], + sortingControl: { + sortingOption: string; + sortingWithinOption: string; + isSortingDsc: string; + isSortingWithinDsc: string; }, +): User[][] { + const authorMap: { [userName: string]: User[] } = {}; + const filtered: User[][] = []; + const { + sortingWithinOption, sortingOption, isSortingDsc, isSortingWithinDsc, + } = sortingControl; + const sortWithinOption = sortingWithinOption === 'title' ? 'searchPath' : sortingWithinOption; + const sortOption = sortingOption === 'groupTitle' ? 'displayName' : sortingOption; + repos.forEach((users) => { + users.forEach((user) => { + if (Object.keys(authorMap).includes(user.name)) { + authorMap[user.name].push(user); + } else { + authorMap[user.name] = [user]; + } + }); + }); + Object.keys(authorMap).forEach((author) => { + if (sortWithinOption === 'totalCommits') { + authorMap[author].sort(window.comparator((repo) => repo.checkedFileTypeContribution ?? 0)); + } else { + authorMap[author].sort(window.comparator((repo) => getComparablePrimitive(repo, sortingWithinOption))); + } + if (isSortingWithinDsc) { + authorMap[author].reverse(); + } + filtered.push(authorMap[author]); + }); + + filtered.sort(window.comparator(sortingHelper, sortOption)); + if (isSortingDsc) { + filtered.reverse(); + } + return filtered; +} + +function sortFiltered( + filtered: User[][], + filterControl: { + filterGroupSelection: FilterGroupSelection; + sortingOption: string; + sortingWithinOption: string; + isSortingDsc: string; + isSortingWithinDsc: string; }, +): User[][] { + const { filterGroupSelection } = filterControl; + let full = []; + + if (filterGroupSelection === 'groupByNone') { + // push all repos into the same group + full[0] = groupByNone(filtered, filterControl); + } else if (filterGroupSelection === 'groupByAuthors') { + full = groupByAuthors(filtered, filterControl); + } else { + full = groupByRepos(filtered, filterControl); + } + + return full; +} + +export default sortFiltered; From 745042566c964b3ff26fda3830de07396754521b Mon Sep 17 00:00:00 2001 From: jq1836 <95712150+jq1836@users.noreply.github.com> Date: Sat, 28 Oct 2023 22:37:56 +0800 Subject: [PATCH 3/4] [#1936] Migrate safari_date.js to typescript (#2053) Currently, there is still some JavaScript code which remains unmigrated. This allows for type unsafe code to be written, potentially resulting in unintended behavior. Let's migrate safari_date.js to TypeScript code to facilitate future changes to the code. --- frontend/src/utils/safari_date.js | 49 ---------------------------- frontend/src/utils/safari_date.ts | 54 +++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 49 deletions(-) delete mode 100644 frontend/src/utils/safari_date.js create mode 100644 frontend/src/utils/safari_date.ts diff --git a/frontend/src/utils/safari_date.js b/frontend/src/utils/safari_date.js deleted file mode 100644 index 117353ed11..0000000000 --- a/frontend/src/utils/safari_date.js +++ /dev/null @@ -1,49 +0,0 @@ -// date keys for handling safari date input // -function isIntegerKey(key) { - return (key >= 48 && key <= 57) || (key >= 96 && key <= 105); -} - -function isArrowOrEnterKey(key) { - return (key >= 37 && key <= 40) || key === 13; -} - -function isBackSpaceOrDeleteKey(key) { - return key === 8 || key === 46; -} - -function validateInputDate(event) { - const key = event.keyCode; - // only allow integer, backspace, delete, arrow or enter keys - if (!(isIntegerKey(key) || isBackSpaceOrDeleteKey(key) || isArrowOrEnterKey(key))) { - event.preventDefault(); - } -} - -function deleteDashInputDate(event) { - const key = event.keyCode; - const date = event.target.value; - // remove two chars before the cursor's position if deleting dash character - if (isBackSpaceOrDeleteKey(key)) { - const cursorPosition = event.target.selectionStart; - if (date[cursorPosition - 1] === '-') { - event.target.value = date.slice(0, cursorPosition - 1); - } - } -} - -window.formatInputDateOnKeyDown = function formatInputDateOnKeyDown(event) { - validateInputDate(event); - deleteDashInputDate(event); -}; - -window.appendDashInputDate = function appendDashInputDate(event) { - const date = event.target.value; - // append dash to date with format yyyy-mm-dd - if (date.match(/^\d{4}$/) !== null) { - event.target.value = `${event.target.value}-`; - } else if (date.match(/^\d{4}-\d{2}$/) !== null) { - event.target.value = `${event.target.value}-`; - } -}; - -export default 'test'; diff --git a/frontend/src/utils/safari_date.ts b/frontend/src/utils/safari_date.ts new file mode 100644 index 0000000000..a670453aa9 --- /dev/null +++ b/frontend/src/utils/safari_date.ts @@ -0,0 +1,54 @@ +// date keys for handling safari date input // +function isIntegerKey(key: string) { + return !Number.isNaN(+key); +} + +function isArrowOrEnterKey(key: string) { + return key === 'ArrowDown' || key === 'ArrowLeft' || key === 'ArrowRight' || key === 'ArrowUp' || key === 'Enter'; +} + +function isBackSpaceOrDeleteKey(key: string) { + return key === 'Backspace' || key === 'Delete'; +} + +function validateInputDate(event: KeyboardEvent) { + const key = event.key; + // only allow integer, backspace, delete, arrow or enter keys + if (!(isIntegerKey(key) || isBackSpaceOrDeleteKey(key) || isArrowOrEnterKey(key))) { + event.preventDefault(); + } +} + +function deleteDashInputDate(event: KeyboardEvent) { + const key = event.key; + // remove two chars before the cursor's position if deleting dash character + if (isBackSpaceOrDeleteKey(key) && event.target !== null && 'value' in event.target + && 'selectionStart' in event.target) { + const date = event.target.value as string; + const cursorPosition = event.target.selectionStart as number; + if (date[cursorPosition - 1] === '-') { + event.target.value = date.slice(0, cursorPosition - 1); + } + } +} + +function formatInputDateOnKeyDown(event: KeyboardEvent) { + validateInputDate(event); + deleteDashInputDate(event); +} + +function appendDashInputDate(event: KeyboardEvent) { + // append dash to date with format yyyy-mm-dd + if (event.target !== null && 'value' in event.target) { + const date = event.target.value as string; + if (date.match(/^\d{4}$/) !== null) { + event.target.value = `${event.target.value}-`; + } else if (date.match(/^\d{4}-\d{2}$/) !== null) { + event.target.value = `${event.target.value}-`; + } + } +} + +Object.assign(window, { formatInputDateOnKeyDown, appendDashInputDate }); + +export default 'test'; From 056fa5fcd2b13cc5e551a65179feb5d78930b0f9 Mon Sep 17 00:00:00 2001 From: jq1836 <95712150+jq1836@users.noreply.github.com> Date: Sat, 28 Oct 2023 22:51:23 +0800 Subject: [PATCH 4/4] Remove frontend JS lint (#2063) Currently, frontend linter is failing due to lint scripts checking javascript files, the last of which has been removed in PR #2053. Lets update the lint command to exclude javascript files front the check. --- frontend/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 6663a3db8a..549cbed432 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,9 +6,9 @@ "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build", - "lint": "eslint src/**/*.{ts,js,vue} cypress/tests/**/*.js cypress/support.js && stylelint ./src/**/*.{vue,scss} && npm run puglint", + "lint": "eslint src/**/*.{ts,vue} cypress/tests/**/*.js cypress/support.js && stylelint ./src/**/*.{vue,scss} && npm run puglint", "devbuild": "vue-cli-service build --mode development", - "lintfix": "eslint --fix src/**/*.{ts,js,vue} cypress/tests/**/*.js cypress/support.js && stylelint --fix ./src/**/*.{vue,scss}", + "lintfix": "eslint --fix src/**/*.{ts,vue} cypress/tests/**/*.js cypress/support.js && stylelint --fix ./src/**/*.{vue,scss}", "puglint": "pug-lint-vue src", "serveOpen": "vue-cli-service serve --open" },