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

Custom Project Name List #267

Merged
merged 7 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
14 changes: 8 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
93 changes: 93 additions & 0 deletions src/components/CustomProjectNameList.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
145 changes: 86 additions & 59 deletions src/components/Options.tsx
Original file line number Diff line number Diff line change
@@ -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 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 {
Expand All @@ -19,6 +20,7 @@ export default function Options(): JSX.Element {
allowList: [],
apiKey: '',
apiUrl: config.apiUrl,
customProjectNames: [],
denyList: [],
extensionStatus: 'allGood',
hostname: '',
Expand All @@ -31,37 +33,42 @@ export default function Options(): JSX.Element {
trackSocialMedia: config.trackSocialMedia,
});

const isApiKeyValid = useMemo(() => apiKeyInvalid(state.apiKey) === '', [state.apiKey]);

const loggingStyleRef = useRef(null);

const restoreSettings = async (): Promise<void> => {
const restoreSettings = useCallback(async () => {
const settings = await getSettings();
setState({
...state,
setState((oldState) => ({
...oldState,
...settings,
});
};
}));
}, []);

useEffect(() => {
void restoreSettings();
}, []);
}, [restoreSettings]);

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);
}
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,
});
Expand All @@ -72,54 +79,64 @@ export default function Options(): JSX.Element {
}
};

const updateDenyListState = (sites: string) => {
setState({
...state,
denyList: sites.trim().split('\n'),
});
};
const updateDenyListState = useCallback((denyList: string[]) => {
setState((oldState) => ({
...oldState,
denyList,
}));
}, []);

const updateAllowListState = (sites: string) => {
setState({
...state,
allowList: sites.trim().split('\n'),
});
};
const updateAllowListState = useCallback((allowList: string[]) => {
setState((oldState) => ({
...oldState,
allowList,
}));
}, []);

const updateLoggingStyle = (style: string) => {
setState({
...state,
const updateCustomProjectNamesState = useCallback((customProjectNames: ProjectName[]) => {
setState((oldState) => ({
...oldState,
customProjectNames,
}));
}, []);

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 = function () {
const loggingStyle = useCallback(() => {
// TODO: rewrite SitesList to be structured inputs instead of textarea

if (state.loggingStyle == 'deny') {
return (
<SitesList
handleChange={updateDenyListState}
label="Exclude"
sites={state.denyList.join('\n')}
sites={state.denyList}
helpText="Sites that you don't want to show in your reports."
/>
);
Expand All @@ -128,14 +145,18 @@ export default function Options(): JSX.Element {
<SitesList
handleChange={updateAllowListState}
label="Include"
sites={state.allowList.join('\n')}
placeholder="http://google.com&#10;http://myproject.com/MyProject"
helpText="Only track these sites. You can assign URL to project by adding @@YourProject at the end of line."
sites={state.allowList}
projectNamePlaceholder="http://google.com&#10;http://myproject.com/MyProject"
helpText="Only track these sites."
/>
);
};

const isApiKeyValid = apiKeyInvalid(state.apiKey) === '';
}, [
state.allowList,
state.denyList,
state.loggingStyle,
updateAllowListState,
updateDenyListState,
]);

return (
<div className="container">
Expand Down Expand Up @@ -168,8 +189,8 @@ export default function Options(): JSX.Element {
value={state.loggingStyle}
onChange={(e) => updateLoggingStyle(e.target.value)}
>
<option value="denyList">All except excluded sites</option>
<option value="allowList">Only allowed sites</option>
<option value="deny">All except excluded sites</option>
<option value="allow">Only allowed sites</option>
</select>
</div>

Expand Down Expand Up @@ -221,6 +242,13 @@ export default function Options(): JSX.Element {
</span>
</div>

<CustomProjectNameList
sites={state.customProjectNames}
label="Custom Project Names"
handleChange={updateCustomProjectNamesState}
helpText=""
/>

<div className="form-group mb-4">
<label htmlFor="apiUrl" className="form-label mb-0">
API Url
Expand Down Expand Up @@ -277,16 +305,15 @@ export default function Options(): JSX.Element {
</div>
<div className="modal-body">
<SitesList
handleChange={(sites: string) => {
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}
/>
</div>
<div className="modal-footer">
Expand Down
Loading
Loading