From 391b75e0c3e9e503e976d3377ac1bf8ea04f28ac Mon Sep 17 00:00:00 2001 From: Rohid Date: Thu, 29 Aug 2024 14:28:16 +0600 Subject: [PATCH 1/6] react-hooks eslint --- .eslintrc.js | 1 + package-lock.json | 14 ++++++----- package.json | 1 + src/components/Options.tsx | 48 +++++++++++++++++++++++-------------- src/components/WakaTime.tsx | 8 ++++++- src/core/WakaTimeCore.ts | 1 + 6 files changed, 48 insertions(+), 25 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 9f4ca737..735ab2cc 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -21,6 +21,7 @@ module.exports = { 'plugin:react/recommended', 'plugin:react/jsx-runtime', 'plugin:typescript-sort-keys/recommended', + 'plugin:react-hooks/recommended', ], globals: { browser: true, diff --git a/package-lock.json b/package-lock.json index d26469ca..6ec6dbad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,6 +71,7 @@ "eslint-plugin-jest-dom": "^4.0.3", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.32.0", + "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-sort-keys-fix": "^1.1.2", "eslint-plugin-testing-library": "^5.9.1", "eslint-plugin-typescript-sort-keys": "^2.1.0", @@ -9693,10 +9694,11 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", - "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -30036,9 +30038,9 @@ } }, "eslint-plugin-react-hooks": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", - "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", "dev": true, "requires": {} }, diff --git a/package.json b/package.json index 251cb8a0..ed7b38da 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "eslint-plugin-jest-dom": "^4.0.3", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.32.0", + "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-sort-keys-fix": "^1.1.2", "eslint-plugin-testing-library": "^5.9.1", "eslint-plugin-typescript-sort-keys": "^2.1.0", diff --git a/src/components/Options.tsx b/src/components/Options.tsx index 6dac9464..c114ac89 100644 --- a/src/components/Options.tsx +++ b/src/components/Options.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import config, { SuccessOrFailType } from '../config/config'; import apiKeyInvalid from '../utils/apiKey'; import { IS_CHROME } from '../utils/operatingSystem'; @@ -33,17 +33,17 @@ export default function Options(): JSX.Element { const loggingStyleRef = useRef(null); - const restoreSettings = async (): Promise => { + const restoreSettings = useCallback(async (): Promise => { const settings = await getSettings(); setState({ ...state, ...settings, }); - }; + }, [state]); useEffect(() => { void restoreSettings(); - }, []); + }, [restoreSettings]); const handleSubmit = async () => { if (state.loading) return; @@ -72,19 +72,25 @@ export default function Options(): JSX.Element { } }; - const updateDenyListState = (sites: string) => { - setState({ - ...state, - denyList: sites.trim().split('\n'), - }); - }; + const updateDenyListState = useCallback( + (sites: string) => { + setState({ + ...state, + denyList: sites.trim().split('\n'), + }); + }, + [state], + ); - const updateAllowListState = (sites: string) => { - setState({ - ...state, - allowList: sites.trim().split('\n'), - }); - }; + const updateAllowListState = useCallback( + (sites: string) => { + setState({ + ...state, + allowList: sites.trim().split('\n'), + }); + }, + [state], + ); const updateLoggingStyle = (style: string) => { setState({ @@ -111,7 +117,7 @@ export default function Options(): JSX.Element { setState({ ...state, trackSocialMedia: !state.trackSocialMedia }); }; - const loggingStyle = function () { + const loggingStyle = useCallback(() => { // TODO: rewrite SitesList to be structured inputs instead of textarea if (state.loggingStyle == 'deny') { @@ -133,7 +139,13 @@ export default function Options(): JSX.Element { helpText="Only track these sites. You can assign URL to project by adding @@YourProject at the end of line." /> ); - }; + }, [ + state.allowList, + state.denyList, + state.loggingStyle, + updateAllowListState, + updateDenyListState, + ]); const isApiKeyValid = apiKeyInvalid(state.apiKey) === ''; diff --git a/src/components/WakaTime.tsx b/src/components/WakaTime.tsx index ff30c797..13436a45 100644 --- a/src/components/WakaTime.tsx +++ b/src/components/WakaTime.tsx @@ -21,10 +21,16 @@ export default function WakaTime(): JSX.Element { useEffect(() => { const fetchData = async () => { await fetchUserData(apiKeyFromRedux, dispatch); + }; + void fetchData(); + }, [apiKeyFromRedux, dispatch]); + + useEffect(() => { + const init = async () => { const items = await browser.storage.sync.get({ extensionStatus: '' }); setExtensionStatus(items.extensionStatus as string); }; - void fetchData(); + void init(); }, []); const isApiKeyValid = apiKeyInvalid(apiKeyFromRedux) === ''; diff --git a/src/core/WakaTimeCore.ts b/src/core/WakaTimeCore.ts index 0f253bd6..fae221be 100644 --- a/src/core/WakaTimeCore.ts +++ b/src/core/WakaTimeCore.ts @@ -147,6 +147,7 @@ class WakaTimeCore { branch: heartbeat?.branch ?? '<>', category: heartbeat?.category, entity: heartbeat?.entity ?? entity, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call id: uuid4(), language: heartbeat?.language, plugin: heartbeat?.plugin, From fc3fd04834b2244e6065cb3f383eb4c43c16baab Mon Sep 17 00:00:00 2001 From: Rohid Date: Thu, 29 Aug 2024 14:37:20 +0600 Subject: [PATCH 2/6] prevent infinite update --- src/components/Options.tsx | 87 ++++++++++++++++++-------------------- 1 file changed, 42 insertions(+), 45 deletions(-) diff --git a/src/components/Options.tsx b/src/components/Options.tsx index c114ac89..f537e723 100644 --- a/src/components/Options.tsx +++ b/src/components/Options.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import config, { SuccessOrFailType } from '../config/config'; import apiKeyInvalid from '../utils/apiKey'; import { IS_CHROME } from '../utils/operatingSystem'; @@ -31,15 +31,17 @@ export default function Options(): JSX.Element { trackSocialMedia: config.trackSocialMedia, }); + const isApiKeyValid = useMemo(() => apiKeyInvalid(state.apiKey) === '', [state.apiKey]); + const loggingStyleRef = useRef(null); - const restoreSettings = useCallback(async (): Promise => { + const restoreSettings = useCallback(async () => { const settings = await getSettings(); - setState({ - ...state, + setState((oldState) => ({ + ...oldState, ...settings, - }); - }, [state]); + })); + }, []); useEffect(() => { void restoreSettings(); @@ -47,7 +49,7 @@ export default function Options(): JSX.Element { const handleSubmit = async () => { if (state.loading) return; - setState({ ...state, loading: true }); + setState((oldState) => ({ ...oldState, loading: true })); if (state.apiUrl.endsWith('/')) { state.apiUrl = state.apiUrl.slice(0, -1); } @@ -72,50 +74,47 @@ export default function Options(): JSX.Element { } }; - const updateDenyListState = useCallback( - (sites: string) => { - setState({ - ...state, - denyList: sites.trim().split('\n'), - }); - }, - [state], - ); + const updateDenyListState = useCallback((sites: string) => { + setState((oldState) => ({ + ...oldState, + denyList: sites.trim().split('\n'), + })); + }, []); - const updateAllowListState = useCallback( - (sites: string) => { - setState({ - ...state, - allowList: sites.trim().split('\n'), - }); - }, - [state], - ); + const updateAllowListState = useCallback((sites: string) => { + setState((oldState) => ({ + ...oldState, + allowList: sites.trim().split('\n'), + })); + }, []); - const updateLoggingStyle = (style: string) => { - setState({ - ...state, + const updateLoggingStyle = useCallback((style: string) => { + setState((oldState) => ({ + ...oldState, loggingStyle: style === 'allow' ? 'allow' : 'deny', - }); - }; + })); + }, []); - const updateLoggingType = (type: string) => { - setState({ - ...state, + const updateLoggingType = useCallback((type: string) => { + setState((oldState) => ({ + ...oldState, loggingType: type === 'url' ? 'url' : 'domain', - }); - }; + })); + }, []); - const updateTheme = (theme: string) => { - setState({ - ...state, + const updateTheme = useCallback((theme: string) => { + setState((oldState) => ({ + ...oldState, theme: theme === 'light' ? 'light' : 'dark', - }); - }; + })); + }, []); - const toggleSocialMedia = () => { - setState({ ...state, trackSocialMedia: !state.trackSocialMedia }); - }; + const toggleSocialMedia = useCallback(() => { + setState((oldState) => ({ + ...oldState, + trackSocialMedia: !oldState.trackSocialMedia, + })); + }, []); const loggingStyle = useCallback(() => { // TODO: rewrite SitesList to be structured inputs instead of textarea @@ -147,8 +146,6 @@ export default function Options(): JSX.Element { updateDenyListState, ]); - const isApiKeyValid = apiKeyInvalid(state.apiKey) === ''; - return (
From 7edc7efeaa5bfc693a9bacc0a9d40115022b9e6b Mon Sep 17 00:00:00 2001 From: Rohid Date: Thu, 29 Aug 2024 14:40:38 +0600 Subject: [PATCH 3/6] fix Logging Style select field not working --- src/components/Options.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Options.tsx b/src/components/Options.tsx index f537e723..90002b25 100644 --- a/src/components/Options.tsx +++ b/src/components/Options.tsx @@ -177,8 +177,8 @@ export default function Options(): JSX.Element { value={state.loggingStyle} onChange={(e) => updateLoggingStyle(e.target.value)} > - - + +
From 6550d26b80bb1d5f25dee2e28f8b6de76e71612d Mon Sep 17 00:00:00 2001 From: Rohid Date: Fri, 30 Aug 2024 00:08:24 +0600 Subject: [PATCH 4/6] assign custom project name to url --- src/components/CustomProjectNameList.tsx | 93 ++++++++++++++++++++++++ src/components/Options.tsx | 56 +++++++++----- src/components/SitesList.tsx | 81 ++++++++++++++------- src/core/WakaTimeCore.ts | 13 +++- src/utils/settings.ts | 9 +++ 5 files changed, 205 insertions(+), 47 deletions(-) create mode 100644 src/components/CustomProjectNameList.tsx diff --git a/src/components/CustomProjectNameList.tsx b/src/components/CustomProjectNameList.tsx new file mode 100644 index 00000000..b502681b --- /dev/null +++ b/src/components/CustomProjectNameList.tsx @@ -0,0 +1,93 @@ +import React, { useCallback } from 'react'; +import { ProjectName } from '../utils/settings'; + +type Props = { + handleChange: (sites: ProjectName[]) => void; + helpText: string; + label: string; + projectNamePlaceholder?: string; + sites: ProjectName[]; + urlPlaceholder?: string; +}; + +export default function CustomProjectNameList({ + handleChange, + label, + urlPlaceholder, + projectNamePlaceholder, + sites, +}: Props): JSX.Element { + const handleAddNewSite = useCallback(() => { + handleChange([...sites, { projectName: '', url: '' }]); + }, [handleChange, sites]); + + const handleUrlChangeForSite = useCallback( + (event: React.ChangeEvent, index: number) => { + handleChange( + sites.map((item, i) => (i === index ? { ...item, url: event.target.value } : item)), + ); + }, + [handleChange, sites], + ); + + const handleOnProjectNameChange = useCallback( + (event: React.ChangeEvent, index: number) => { + handleChange( + sites.map((item, i) => (i === index ? { ...item, projectName: event.target.value } : item)), + ); + }, + [handleChange, sites], + ); + + const handleRemoveSite = useCallback( + (index: number) => { + handleChange(sites.filter((_, i) => i !== index)); + }, + [handleChange, sites], + ); + + return ( +
+ + + {sites.length > 0 && ( +
+ {sites.map((site, i) => ( +
+
+ handleUrlChangeForSite(e, i)} + /> +
+
+ handleOnProjectNameChange(e, i)} + /> +
+ +
+ ))} +
+ )} + + +
+ ); +} diff --git a/src/components/Options.tsx b/src/components/Options.tsx index 90002b25..33fdee20 100644 --- a/src/components/Options.tsx +++ b/src/components/Options.tsx @@ -2,8 +2,9 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import config, { SuccessOrFailType } from '../config/config'; import apiKeyInvalid from '../utils/apiKey'; import { IS_CHROME } from '../utils/operatingSystem'; -import { getSettings, saveSettings, Settings } from '../utils/settings'; +import { getSettings, ProjectName, saveSettings, Settings } from '../utils/settings'; import { logUserIn } from '../utils/user'; +import CustomProjectNameList from './CustomProjectNameList'; import SitesList from './SitesList'; interface State extends Settings { @@ -19,6 +20,7 @@ export default function Options(): JSX.Element { allowList: [], apiKey: '', apiUrl: config.apiUrl, + customProjectNames: [], denyList: [], extensionStatus: 'allGood', hostname: '', @@ -54,16 +56,19 @@ export default function Options(): JSX.Element { state.apiUrl = state.apiUrl.slice(0, -1); } await saveSettings({ - allowList: state.allowList, + allowList: state.allowList.filter((item) => !!item.trim()), apiKey: state.apiKey, apiUrl: state.apiUrl, - denyList: state.denyList, + customProjectNames: state.customProjectNames.filter( + (item) => !!item.url.trim() && !!item.projectName.trim(), + ), + denyList: state.denyList.filter((item) => !!item.trim()), extensionStatus: state.extensionStatus, hostname: state.hostname, loggingEnabled: state.loggingEnabled, loggingStyle: state.loggingStyle, loggingType: state.loggingType, - socialMediaSites: state.socialMediaSites, + socialMediaSites: state.socialMediaSites.filter((item) => !!item.trim()), theme: state.theme, trackSocialMedia: state.trackSocialMedia, }); @@ -74,17 +79,24 @@ export default function Options(): JSX.Element { } }; - const updateDenyListState = useCallback((sites: string) => { + const updateDenyListState = useCallback((denyList: string[]) => { setState((oldState) => ({ ...oldState, - denyList: sites.trim().split('\n'), + denyList, })); }, []); - const updateAllowListState = useCallback((sites: string) => { + const updateAllowListState = useCallback((allowList: string[]) => { setState((oldState) => ({ ...oldState, - allowList: sites.trim().split('\n'), + allowList, + })); + }, []); + + const updateCustomProjectNamesState = useCallback((customProjectNames: ProjectName[]) => { + setState((oldState) => ({ + ...oldState, + customProjectNames, })); }, []); @@ -124,7 +136,7 @@ export default function Options(): JSX.Element { ); @@ -133,9 +145,9 @@ export default function Options(): JSX.Element { ); }, [ @@ -230,6 +242,13 @@ export default function Options(): JSX.Element {
+ +
{ - setState({ - ...state, - socialMediaSites: sites.split('\n'), - }); + handleChange={(socialMediaSites) => { + setState((oldState) => ({ + ...oldState, + socialMediaSites, + })); }} label="Social" - sites={state.socialMediaSites.join('\n')} + sites={state.socialMediaSites} helpText="Sites that you don't want to show in your reports." - rows={5} />
diff --git a/src/components/SitesList.tsx b/src/components/SitesList.tsx index 86023ca2..2f6ce293 100644 --- a/src/components/SitesList.tsx +++ b/src/components/SitesList.tsx @@ -1,47 +1,74 @@ -import React from 'react'; +import React, { useCallback } from 'react'; type Props = { - handleChange: (sites: string) => void; + handleChange: (sites: string[]) => void; helpText: string; label: string; - placeholder?: string; - rows?: number; - sites: string; + projectNamePlaceholder?: string; + sites: string[]; + urlPlaceholder?: string; }; export default function SitesList({ handleChange, label, - placeholder, - rows, + urlPlaceholder, sites, helpText, }: Props): JSX.Element { - const textareaChange = (event: React.ChangeEvent) => { - handleChange(event.target.value); - }; + const handleAddNewSite = useCallback(() => { + handleChange([...sites, '']); + }, [handleChange, sites]); + + const handleUrlChangeForSite = useCallback( + (event: React.ChangeEvent, index: number) => { + handleChange(sites.map((item, i) => (i === index ? event.target.value : item))); + }, + [handleChange, sites], + ); + + const handleRemoveSite = useCallback( + (index: number) => { + handleChange(sites.filter((_, i) => i !== index)); + }, + [handleChange, sites], + ); return ( -
-