Merge pull request #267 from wakatime/feature/enhanced-input-ui
Custom Project Name List
This commit is contained in:
@@ -21,6 +21,7 @@ module.exports = {
|
|||||||
'plugin:react/recommended',
|
'plugin:react/recommended',
|
||||||
'plugin:react/jsx-runtime',
|
'plugin:react/jsx-runtime',
|
||||||
'plugin:typescript-sort-keys/recommended',
|
'plugin:typescript-sort-keys/recommended',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
],
|
],
|
||||||
globals: {
|
globals: {
|
||||||
browser: true,
|
browser: true,
|
||||||
|
|||||||
14
package-lock.json
generated
14
package-lock.json
generated
@@ -71,6 +71,7 @@
|
|||||||
"eslint-plugin-jest-dom": "^4.0.3",
|
"eslint-plugin-jest-dom": "^4.0.3",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-react": "^7.32.0",
|
"eslint-plugin-react": "^7.32.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.2",
|
||||||
"eslint-plugin-sort-keys-fix": "^1.1.2",
|
"eslint-plugin-sort-keys-fix": "^1.1.2",
|
||||||
"eslint-plugin-testing-library": "^5.9.1",
|
"eslint-plugin-testing-library": "^5.9.1",
|
||||||
"eslint-plugin-typescript-sort-keys": "^2.1.0",
|
"eslint-plugin-typescript-sort-keys": "^2.1.0",
|
||||||
@@ -9693,10 +9694,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-react-hooks": {
|
"node_modules/eslint-plugin-react-hooks": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz",
|
||||||
"integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==",
|
"integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
@@ -30036,9 +30038,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"eslint-plugin-react-hooks": {
|
"eslint-plugin-react-hooks": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz",
|
||||||
"integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==",
|
"integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -92,6 +92,7 @@
|
|||||||
"eslint-plugin-jest-dom": "^4.0.3",
|
"eslint-plugin-jest-dom": "^4.0.3",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-react": "^7.32.0",
|
"eslint-plugin-react": "^7.32.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.2",
|
||||||
"eslint-plugin-sort-keys-fix": "^1.1.2",
|
"eslint-plugin-sort-keys-fix": "^1.1.2",
|
||||||
"eslint-plugin-testing-library": "^5.9.1",
|
"eslint-plugin-testing-library": "^5.9.1",
|
||||||
"eslint-plugin-typescript-sort-keys": "^2.1.0",
|
"eslint-plugin-typescript-sort-keys": "^2.1.0",
|
||||||
|
|||||||
93
src/components/CustomProjectNameList.tsx
Normal file
93
src/components/CustomProjectNameList.tsx
Normal file
@@ -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<HTMLInputElement>, index: number) => {
|
||||||
|
handleChange(
|
||||||
|
sites.map((item, i) => (i === index ? { ...item, url: event.target.value } : item)),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[handleChange, sites],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOnProjectNameChange = useCallback(
|
||||||
|
(event: React.ChangeEvent<HTMLInputElement>, 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 (
|
||||||
|
<div className="form-group mb-4 d-flex flex-column gap-3">
|
||||||
|
<label htmlFor={`${label}-siteList`} className="control-label">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{sites.length > 0 && (
|
||||||
|
<div className="d-flex flex-column gap-2">
|
||||||
|
{sites.map((site, i) => (
|
||||||
|
<div key={i} className="d-flex gap-2">
|
||||||
|
<div className="flex-fill">
|
||||||
|
<input
|
||||||
|
placeholder={urlPlaceholder ?? 'https://google.com'}
|
||||||
|
className="form-control"
|
||||||
|
value={site.url}
|
||||||
|
onChange={(e) => handleUrlChangeForSite(e, i)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-fill">
|
||||||
|
<input
|
||||||
|
placeholder={projectNamePlaceholder ?? 'Project Name'}
|
||||||
|
value={site.projectName}
|
||||||
|
className="form-control"
|
||||||
|
onChange={(e) => handleOnProjectNameChange(e, i)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-sm btn-default"
|
||||||
|
onClick={() => handleRemoveSite(i)}
|
||||||
|
>
|
||||||
|
<i className="fa fa-fw fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button type="button" onClick={handleAddNewSite} className="btn btn-default col-12">
|
||||||
|
<i className="fa fa-fw fa-plus me-2"></i>
|
||||||
|
Add Project Name
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import config, { SuccessOrFailType } from '../config/config';
|
import config, { SuccessOrFailType } from '../config/config';
|
||||||
import apiKeyInvalid from '../utils/apiKey';
|
import apiKeyInvalid from '../utils/apiKey';
|
||||||
import { IS_CHROME } from '../utils/operatingSystem';
|
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 { logUserIn } from '../utils/user';
|
||||||
|
import CustomProjectNameList from './CustomProjectNameList';
|
||||||
import SitesList from './SitesList';
|
import SitesList from './SitesList';
|
||||||
|
|
||||||
interface State extends Settings {
|
interface State extends Settings {
|
||||||
@@ -19,6 +20,7 @@ export default function Options(): JSX.Element {
|
|||||||
allowList: [],
|
allowList: [],
|
||||||
apiKey: '',
|
apiKey: '',
|
||||||
apiUrl: config.apiUrl,
|
apiUrl: config.apiUrl,
|
||||||
|
customProjectNames: [],
|
||||||
denyList: [],
|
denyList: [],
|
||||||
extensionStatus: 'allGood',
|
extensionStatus: 'allGood',
|
||||||
hostname: '',
|
hostname: '',
|
||||||
@@ -31,37 +33,42 @@ export default function Options(): JSX.Element {
|
|||||||
trackSocialMedia: config.trackSocialMedia,
|
trackSocialMedia: config.trackSocialMedia,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isApiKeyValid = useMemo(() => apiKeyInvalid(state.apiKey) === '', [state.apiKey]);
|
||||||
|
|
||||||
const loggingStyleRef = useRef(null);
|
const loggingStyleRef = useRef(null);
|
||||||
|
|
||||||
const restoreSettings = async (): Promise<void> => {
|
const restoreSettings = useCallback(async () => {
|
||||||
const settings = await getSettings();
|
const settings = await getSettings();
|
||||||
setState({
|
setState((oldState) => ({
|
||||||
...state,
|
...oldState,
|
||||||
...settings,
|
...settings,
|
||||||
});
|
}));
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void restoreSettings();
|
void restoreSettings();
|
||||||
}, []);
|
}, [restoreSettings]);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (state.loading) return;
|
if (state.loading) return;
|
||||||
setState({ ...state, loading: true });
|
setState((oldState) => ({ ...oldState, loading: true }));
|
||||||
if (state.apiUrl.endsWith('/')) {
|
if (state.apiUrl.endsWith('/')) {
|
||||||
state.apiUrl = state.apiUrl.slice(0, -1);
|
state.apiUrl = state.apiUrl.slice(0, -1);
|
||||||
}
|
}
|
||||||
await saveSettings({
|
await saveSettings({
|
||||||
allowList: state.allowList,
|
allowList: state.allowList.filter((item) => !!item.trim()),
|
||||||
apiKey: state.apiKey,
|
apiKey: state.apiKey,
|
||||||
apiUrl: state.apiUrl,
|
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,
|
extensionStatus: state.extensionStatus,
|
||||||
hostname: state.hostname,
|
hostname: state.hostname,
|
||||||
loggingEnabled: state.loggingEnabled,
|
loggingEnabled: state.loggingEnabled,
|
||||||
loggingStyle: state.loggingStyle,
|
loggingStyle: state.loggingStyle,
|
||||||
loggingType: state.loggingType,
|
loggingType: state.loggingType,
|
||||||
socialMediaSites: state.socialMediaSites,
|
socialMediaSites: state.socialMediaSites.filter((item) => !!item.trim()),
|
||||||
theme: state.theme,
|
theme: state.theme,
|
||||||
trackSocialMedia: state.trackSocialMedia,
|
trackSocialMedia: state.trackSocialMedia,
|
||||||
});
|
});
|
||||||
@@ -72,46 +79,56 @@ export default function Options(): JSX.Element {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateDenyListState = (sites: string) => {
|
const updateDenyListState = useCallback((denyList: string[]) => {
|
||||||
setState({
|
setState((oldState) => ({
|
||||||
...state,
|
...oldState,
|
||||||
denyList: sites.trim().split('\n'),
|
denyList,
|
||||||
});
|
}));
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const updateAllowListState = (sites: string) => {
|
const updateAllowListState = useCallback((allowList: string[]) => {
|
||||||
setState({
|
setState((oldState) => ({
|
||||||
...state,
|
...oldState,
|
||||||
allowList: sites.trim().split('\n'),
|
allowList,
|
||||||
});
|
}));
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const updateLoggingStyle = (style: string) => {
|
const updateCustomProjectNamesState = useCallback((customProjectNames: ProjectName[]) => {
|
||||||
setState({
|
setState((oldState) => ({
|
||||||
...state,
|
...oldState,
|
||||||
|
customProjectNames,
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateLoggingStyle = useCallback((style: string) => {
|
||||||
|
setState((oldState) => ({
|
||||||
|
...oldState,
|
||||||
loggingStyle: style === 'allow' ? 'allow' : 'deny',
|
loggingStyle: style === 'allow' ? 'allow' : 'deny',
|
||||||
});
|
}));
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const updateLoggingType = (type: string) => {
|
const updateLoggingType = useCallback((type: string) => {
|
||||||
setState({
|
setState((oldState) => ({
|
||||||
...state,
|
...oldState,
|
||||||
loggingType: type === 'url' ? 'url' : 'domain',
|
loggingType: type === 'url' ? 'url' : 'domain',
|
||||||
});
|
}));
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const updateTheme = (theme: string) => {
|
const updateTheme = useCallback((theme: string) => {
|
||||||
setState({
|
setState((oldState) => ({
|
||||||
...state,
|
...oldState,
|
||||||
theme: theme === 'light' ? 'light' : 'dark',
|
theme: theme === 'light' ? 'light' : 'dark',
|
||||||
});
|
}));
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const toggleSocialMedia = () => {
|
const toggleSocialMedia = useCallback(() => {
|
||||||
setState({ ...state, trackSocialMedia: !state.trackSocialMedia });
|
setState((oldState) => ({
|
||||||
};
|
...oldState,
|
||||||
|
trackSocialMedia: !oldState.trackSocialMedia,
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const loggingStyle = function () {
|
const loggingStyle = useCallback(() => {
|
||||||
// TODO: rewrite SitesList to be structured inputs instead of textarea
|
// TODO: rewrite SitesList to be structured inputs instead of textarea
|
||||||
|
|
||||||
if (state.loggingStyle == 'deny') {
|
if (state.loggingStyle == 'deny') {
|
||||||
@@ -119,7 +136,7 @@ export default function Options(): JSX.Element {
|
|||||||
<SitesList
|
<SitesList
|
||||||
handleChange={updateDenyListState}
|
handleChange={updateDenyListState}
|
||||||
label="Exclude"
|
label="Exclude"
|
||||||
sites={state.denyList.join('\n')}
|
sites={state.denyList}
|
||||||
helpText="Sites that you don't want to show in your reports."
|
helpText="Sites that you don't want to show in your reports."
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -128,14 +145,18 @@ export default function Options(): JSX.Element {
|
|||||||
<SitesList
|
<SitesList
|
||||||
handleChange={updateAllowListState}
|
handleChange={updateAllowListState}
|
||||||
label="Include"
|
label="Include"
|
||||||
sites={state.allowList.join('\n')}
|
sites={state.allowList}
|
||||||
placeholder="http://google.com http://myproject.com/MyProject"
|
projectNamePlaceholder="http://google.com http://myproject.com/MyProject"
|
||||||
helpText="Only track these sites. You can assign URL to project by adding @@YourProject at the end of line."
|
helpText="Only track these sites."
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
}, [
|
||||||
|
state.allowList,
|
||||||
const isApiKeyValid = apiKeyInvalid(state.apiKey) === '';
|
state.denyList,
|
||||||
|
state.loggingStyle,
|
||||||
|
updateAllowListState,
|
||||||
|
updateDenyListState,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container">
|
<div className="container">
|
||||||
@@ -168,8 +189,8 @@ export default function Options(): JSX.Element {
|
|||||||
value={state.loggingStyle}
|
value={state.loggingStyle}
|
||||||
onChange={(e) => updateLoggingStyle(e.target.value)}
|
onChange={(e) => updateLoggingStyle(e.target.value)}
|
||||||
>
|
>
|
||||||
<option value="denyList">All except excluded sites</option>
|
<option value="deny">All except excluded sites</option>
|
||||||
<option value="allowList">Only allowed sites</option>
|
<option value="allow">Only allowed sites</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -221,6 +242,13 @@ export default function Options(): JSX.Element {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<CustomProjectNameList
|
||||||
|
sites={state.customProjectNames}
|
||||||
|
label="Custom Project Names"
|
||||||
|
handleChange={updateCustomProjectNamesState}
|
||||||
|
helpText=""
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="form-group mb-4">
|
<div className="form-group mb-4">
|
||||||
<label htmlFor="apiUrl" className="form-label mb-0">
|
<label htmlFor="apiUrl" className="form-label mb-0">
|
||||||
API Url
|
API Url
|
||||||
@@ -277,16 +305,15 @@ export default function Options(): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
<div className="modal-body">
|
<div className="modal-body">
|
||||||
<SitesList
|
<SitesList
|
||||||
handleChange={(sites: string) => {
|
handleChange={(socialMediaSites) => {
|
||||||
setState({
|
setState((oldState) => ({
|
||||||
...state,
|
...oldState,
|
||||||
socialMediaSites: sites.split('\n'),
|
socialMediaSites,
|
||||||
});
|
}));
|
||||||
}}
|
}}
|
||||||
label="Social"
|
label="Social"
|
||||||
sites={state.socialMediaSites.join('\n')}
|
sites={state.socialMediaSites}
|
||||||
helpText="Sites that you don't want to show in your reports."
|
helpText="Sites that you don't want to show in your reports."
|
||||||
rows={5}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-footer">
|
<div className="modal-footer">
|
||||||
|
|||||||
@@ -1,47 +1,74 @@
|
|||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handleChange: (sites: string) => void;
|
handleChange: (sites: string[]) => void;
|
||||||
helpText: string;
|
helpText: string;
|
||||||
label: string;
|
label: string;
|
||||||
placeholder?: string;
|
projectNamePlaceholder?: string;
|
||||||
rows?: number;
|
sites: string[];
|
||||||
sites: string;
|
urlPlaceholder?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SitesList({
|
export default function SitesList({
|
||||||
handleChange,
|
handleChange,
|
||||||
label,
|
label,
|
||||||
placeholder,
|
urlPlaceholder,
|
||||||
rows,
|
|
||||||
sites,
|
sites,
|
||||||
helpText,
|
helpText,
|
||||||
}: Props): JSX.Element {
|
}: Props): JSX.Element {
|
||||||
const textareaChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const handleAddNewSite = useCallback(() => {
|
||||||
handleChange(event.target.value);
|
handleChange([...sites, '']);
|
||||||
};
|
}, [handleChange, sites]);
|
||||||
|
|
||||||
|
const handleUrlChangeForSite = useCallback(
|
||||||
|
(event: React.ChangeEvent<HTMLInputElement>, 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 (
|
return (
|
||||||
<div className="form-group mb-4">
|
<div className="form-group mb-4 d-flex flex-column gap-2">
|
||||||
<label htmlFor={`${label}-siteList`} className="col-lg-2 control-label">
|
<label htmlFor={`${label}-siteList`} className="control-label">
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className="col-lg-10">
|
{sites.length > 0 && (
|
||||||
<textarea
|
<div className="d-flex flex-column gap-2">
|
||||||
id={`${label}-siteList`}
|
{sites.map((site, i) => (
|
||||||
|
<div key={i} className="d-flex gap-2">
|
||||||
|
<div className="flex-fill">
|
||||||
|
<input
|
||||||
|
placeholder={urlPlaceholder ?? 'https://google.com'}
|
||||||
className="form-control"
|
className="form-control"
|
||||||
rows={rows ?? 3}
|
value={site}
|
||||||
onChange={textareaChange}
|
onChange={(e) => handleUrlChangeForSite(e, i)}
|
||||||
placeholder={placeholder ?? 'http://google.com'}
|
/>
|
||||||
value={sites}
|
|
||||||
></textarea>
|
|
||||||
<span className="text-secondary">
|
|
||||||
{helpText}
|
|
||||||
<br />
|
|
||||||
One line per site.
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-sm btn-default"
|
||||||
|
onClick={() => handleRemoveSite(i)}
|
||||||
|
>
|
||||||
|
<i className="fa fa-fw fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button type="button" onClick={handleAddNewSite} className="btn btn-default col-12">
|
||||||
|
<i className="fa fa-fw fa-plus me-2"></i>
|
||||||
|
Add Site
|
||||||
|
</button>
|
||||||
|
<span className="text-secondary">{helpText}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,10 +21,16 @@ export default function WakaTime(): JSX.Element {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
await fetchUserData(apiKeyFromRedux, dispatch);
|
await fetchUserData(apiKeyFromRedux, dispatch);
|
||||||
|
};
|
||||||
|
void fetchData();
|
||||||
|
}, [apiKeyFromRedux, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const init = async () => {
|
||||||
const items = await browser.storage.sync.get({ extensionStatus: '' });
|
const items = await browser.storage.sync.get({ extensionStatus: '' });
|
||||||
setExtensionStatus(items.extensionStatus as string);
|
setExtensionStatus(items.extensionStatus as string);
|
||||||
};
|
};
|
||||||
void fetchData();
|
void init();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isApiKeyValid = apiKeyInvalid(apiKeyFromRedux) === '';
|
const isApiKeyValid = apiKeyInvalid(apiKeyFromRedux) === '';
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ describe('wakatime config', () => {
|
|||||||
"chrome://",
|
"chrome://",
|
||||||
"about:",
|
"about:",
|
||||||
],
|
],
|
||||||
|
"queueName": "heartbeatQueue",
|
||||||
"socialMediaSites": [
|
"socialMediaSites": [
|
||||||
"facebook.com",
|
"facebook.com",
|
||||||
"instagram.com",
|
"instagram.com",
|
||||||
|
|||||||
@@ -81,6 +81,14 @@ class WakaTimeCore {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getProjectNameFromList(url: string, settings: Settings) {
|
||||||
|
const site = settings.customProjectNames.find((pattern) => {
|
||||||
|
const re = new RegExp(pattern.url);
|
||||||
|
return re.test(url);
|
||||||
|
});
|
||||||
|
return site?.projectName;
|
||||||
|
}
|
||||||
|
|
||||||
async handleActivity(tabId: number) {
|
async handleActivity(tabId: number) {
|
||||||
const settings = await getSettings();
|
const settings = await getSettings();
|
||||||
if (!settings.loggingEnabled) {
|
if (!settings.loggingEnabled) {
|
||||||
@@ -135,14 +143,18 @@ class WakaTimeCore {
|
|||||||
).heartbeat;
|
).heartbeat;
|
||||||
|
|
||||||
const entity = settings.loggingType === 'domain' ? getDomainFromUrl(url) : url;
|
const entity = settings.loggingType === 'domain' ? getDomainFromUrl(url) : url;
|
||||||
|
|
||||||
|
const projectNameFromList = this.getProjectNameFromList(url, settings);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
branch: heartbeat?.branch ?? '<<LAST_BRANCH>>',
|
branch: heartbeat?.branch ?? '<<LAST_BRANCH>>',
|
||||||
category: heartbeat?.category,
|
category: heartbeat?.category,
|
||||||
entity: heartbeat?.entity ?? entity,
|
entity: heartbeat?.entity ?? entity,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
|
||||||
id: uuid4(),
|
id: uuid4(),
|
||||||
language: heartbeat?.language,
|
language: heartbeat?.language,
|
||||||
plugin: heartbeat?.plugin,
|
plugin: heartbeat?.plugin,
|
||||||
project: heartbeat?.project ?? '<<LAST_PROJECT>>',
|
project: projectNameFromList ?? heartbeat?.project ?? '<<LAST_PROJECT>>',
|
||||||
time: this.getCurrentTime(),
|
time: this.getCurrentTime(),
|
||||||
type: heartbeat?.entityType ?? (settings.loggingType as EntityType),
|
type: heartbeat?.entityType ?? (settings.loggingType as EntityType),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import browser from 'webextension-polyfill';
|
import browser from 'webextension-polyfill';
|
||||||
import config, { ExtensionStatus, LoggingStyle, LoggingType, Theme } from '../config/config';
|
import config, { ExtensionStatus, LoggingStyle, LoggingType, Theme } from '../config/config';
|
||||||
|
|
||||||
|
export interface ProjectName {
|
||||||
|
projectName: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
allowList: string[];
|
allowList: string[];
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
apiUrl: string;
|
apiUrl: string;
|
||||||
|
customProjectNames: ProjectName[];
|
||||||
denyList: string[];
|
denyList: string[];
|
||||||
extensionStatus: ExtensionStatus;
|
extensionStatus: ExtensionStatus;
|
||||||
hostname: string;
|
hostname: string;
|
||||||
@@ -22,6 +28,7 @@ export const getSettings = async (): Promise<Settings> => {
|
|||||||
apiKey: config.apiKey,
|
apiKey: config.apiKey,
|
||||||
apiUrl: config.apiUrl,
|
apiUrl: config.apiUrl,
|
||||||
blacklist: null,
|
blacklist: null,
|
||||||
|
customProjectNames: [],
|
||||||
denyList: [],
|
denyList: [],
|
||||||
hostname: config.hostname,
|
hostname: config.hostname,
|
||||||
loggingEnabled: config.loggingEnabled,
|
loggingEnabled: config.loggingEnabled,
|
||||||
@@ -48,6 +55,7 @@ export const getSettings = async (): Promise<Settings> => {
|
|||||||
await browser.storage.sync.set({ denyList: settings.denyList });
|
await browser.storage.sync.set({ denyList: settings.denyList });
|
||||||
await browser.storage.sync.remove('blacklist');
|
await browser.storage.sync.remove('blacklist');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof settings.socialMediaSites === 'string') {
|
if (typeof settings.socialMediaSites === 'string') {
|
||||||
settings.socialMediaSites = settings.socialMediaSites.trim().split('\n');
|
settings.socialMediaSites = settings.socialMediaSites.trim().split('\n');
|
||||||
await browser.storage.sync.set({
|
await browser.storage.sync.set({
|
||||||
@@ -59,6 +67,7 @@ export const getSettings = async (): Promise<Settings> => {
|
|||||||
allowList: settings.allowList,
|
allowList: settings.allowList,
|
||||||
apiKey: settings.apiKey,
|
apiKey: settings.apiKey,
|
||||||
apiUrl: settings.apiUrl,
|
apiUrl: settings.apiUrl,
|
||||||
|
customProjectNames: settings.customProjectNames,
|
||||||
denyList: settings.denyList,
|
denyList: settings.denyList,
|
||||||
extensionStatus: settings.extensionStatus,
|
extensionStatus: settings.extensionStatus,
|
||||||
hostname: settings.hostname,
|
hostname: settings.hostname,
|
||||||
|
|||||||
@@ -40,8 +40,6 @@ chrome.runtime.onMessage.addListener(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const heartbeat = site.parser(request.url);
|
|
||||||
|
|
||||||
sendResponse({ heartbeat: site.parser(request.url) });
|
sendResponse({ heartbeat: site.parser(request.url) });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user