Es6 cmp migration (#113)
* migrate Alert component * convert Mainlist component * add webpack watch task * update build script for different manifests * add types for api responses * convert changeExtensionIcon * convert inArray, getDomainParts, contains to ts * convert changeExtensionTooltip * convert changeExtensionState to ts
This commit is contained in:
34
src/components/Alert.test.tsx
Normal file
34
src/components/Alert.test.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import Alert from './Alert';
|
||||
|
||||
describe('Alert Component', () => {
|
||||
it('should render with proper text on success type', () => {
|
||||
const text = 'Test Text';
|
||||
const { container } = render(<Alert text={text} type="success" />);
|
||||
expect(screen.getByText(text)).toBeTruthy();
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
<div
|
||||
class="alert alert-success"
|
||||
>
|
||||
Test Text
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
it('should render wtih proper text on danger type', () => {
|
||||
const text = 'Test Text';
|
||||
const { container } = render(<Alert text={text} type="danger" />);
|
||||
expect(screen.getByText(text)).toBeTruthy();
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
<div
|
||||
class="alert alert-danger"
|
||||
>
|
||||
Test Text
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
});
|
||||
11
src/components/Alert.tsx
Normal file
11
src/components/Alert.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { SuccessOrFailType } from '../config';
|
||||
interface AlertProps {
|
||||
text: string;
|
||||
type: SuccessOrFailType;
|
||||
}
|
||||
|
||||
export default function Alert({ type, text }: AlertProps): JSX.Element {
|
||||
return <div className={classNames('alert', `alert-${type}`)}>{text}</div>;
|
||||
}
|
||||
63
src/components/MainList.test.tsx
Normal file
63
src/components/MainList.test.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import MainList from './MainList';
|
||||
|
||||
type onClick = (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void;
|
||||
describe('MainList', () => {
|
||||
let disableLogging: onClick;
|
||||
let enableLogging: onClick;
|
||||
let loggedIn: boolean;
|
||||
let loggingEnabled: boolean;
|
||||
let logoutUser: onClick;
|
||||
let totalTimeLoggedToday: string;
|
||||
beforeEach(() => {
|
||||
disableLogging = jest.fn();
|
||||
enableLogging = jest.fn();
|
||||
loggingEnabled = false;
|
||||
loggedIn = false;
|
||||
logoutUser = jest.fn();
|
||||
totalTimeLoggedToday = '1/1/1999';
|
||||
});
|
||||
it('should render properly', () => {
|
||||
const { container } = render(
|
||||
<MainList
|
||||
disableLogging={disableLogging}
|
||||
enableLogging={enableLogging}
|
||||
loggingEnabled={loggingEnabled}
|
||||
loggedIn={loggedIn}
|
||||
logoutUser={logoutUser}
|
||||
totalTimeLoggedToday={totalTimeLoggedToday}
|
||||
/>,
|
||||
);
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
class="list-group"
|
||||
>
|
||||
<a
|
||||
class="list-group-item"
|
||||
href="#"
|
||||
>
|
||||
<i
|
||||
class="fa fa-fw fa-cogs"
|
||||
/>
|
||||
Options
|
||||
</a>
|
||||
<a
|
||||
class="list-group-item"
|
||||
href="https://wakatime.com/login"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<i
|
||||
class="fa fa-fw fa-sign-in"
|
||||
/>
|
||||
Login
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
});
|
||||
87
src/components/MainList.tsx
Normal file
87
src/components/MainList.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React from 'react';
|
||||
import { browser } from 'webextension-polyfill-ts';
|
||||
|
||||
export interface MainListProps {
|
||||
disableLogging: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void;
|
||||
enableLogging: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void;
|
||||
loggedIn: boolean;
|
||||
loggingEnabled: boolean;
|
||||
logoutUser: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void;
|
||||
totalTimeLoggedToday?: string;
|
||||
}
|
||||
const openOptionsPage = async (): Promise<void> => {
|
||||
await browser.runtime.openOptionsPage();
|
||||
};
|
||||
|
||||
export default function MainList({
|
||||
disableLogging,
|
||||
enableLogging,
|
||||
loggedIn,
|
||||
loggingEnabled,
|
||||
logoutUser,
|
||||
totalTimeLoggedToday,
|
||||
}: MainListProps): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
{loggedIn && (
|
||||
<div className="row">
|
||||
<div className="col-xs-12">
|
||||
<blockquote>
|
||||
<p>{totalTimeLoggedToday}</p>
|
||||
<small>
|
||||
<cite>TOTAL TIME LOGGED TODAY</cite>
|
||||
</small>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{loggingEnabled && loggedIn && (
|
||||
<div className="row">
|
||||
<div className="col-xs-12">
|
||||
<p>
|
||||
<a href="#" onClick={disableLogging} className="btn btn-danger btn-block">
|
||||
Disable logging
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!loggingEnabled && loggedIn && (
|
||||
<div className="row">
|
||||
<div className="col-xs-12">
|
||||
<p>
|
||||
<a href="#" onClick={enableLogging} className="btn btn-success btn-block">
|
||||
Enable logging
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="list-group">
|
||||
<a href="#" className="list-group-item" onClick={openOptionsPage}>
|
||||
<i className="fa fa-fw fa-cogs"></i>
|
||||
Options
|
||||
</a>
|
||||
{loggedIn && (
|
||||
<div>
|
||||
<a href="#" className="list-group-item" onClick={logoutUser}>
|
||||
<i className="fa fa-fw fa-sign-out"></i>
|
||||
Logout
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{!loggedIn && (
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://wakatime.com/login"
|
||||
className="list-group-item"
|
||||
>
|
||||
<i className="fa fa-fw fa-sign-in"></i>
|
||||
Login
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,16 +3,16 @@ import { browser } from 'webextension-polyfill-ts';
|
||||
/**
|
||||
* Logging
|
||||
*/
|
||||
type ApiStates = 'allGood' | 'notLogging' | 'notSignedIn' | 'blacklisted' | 'whitelisted';
|
||||
export type ApiStates = 'allGood' | 'notLogging' | 'notSignedIn' | 'blacklisted' | 'whitelisted';
|
||||
/**
|
||||
* Supported logging style
|
||||
*/
|
||||
type LoggingStyle = 'whitelist' | 'blacklist';
|
||||
export type LoggingStyle = 'whitelist' | 'blacklist';
|
||||
/**
|
||||
* Logging type
|
||||
*/
|
||||
type LoggingType = 'domain' | 'url';
|
||||
type SuccessOrFailType = 'success' | 'danger';
|
||||
export type LoggingType = 'domain' | 'url';
|
||||
export type SuccessOrFailType = 'success' | 'danger';
|
||||
/**
|
||||
* Predefined alert type and text for success and failure.
|
||||
*/
|
||||
@@ -31,10 +31,10 @@ interface SuccessOrFailAlert {
|
||||
* Different colors for different states of the extension
|
||||
*/
|
||||
interface Colors {
|
||||
allGood: string;
|
||||
lightTheme: string;
|
||||
notLogging: string;
|
||||
notSignedIn: string;
|
||||
allGood: '';
|
||||
lightTheme: 'white';
|
||||
notLogging: 'gray';
|
||||
notSignedIn: 'red';
|
||||
}
|
||||
/**
|
||||
* Tooltip messages
|
||||
|
||||
38
src/manifests/chrome.json
Normal file
38
src/manifests/chrome.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"background": {
|
||||
"persistent": false,
|
||||
"scripts": ["background.js"]
|
||||
},
|
||||
"browser_action": {
|
||||
"default_icon": {
|
||||
"19": "graphics/wakatime-logo-19.png",
|
||||
"38": "graphics/wakatime-logo-38.png"
|
||||
},
|
||||
"default_popup": "popup.html",
|
||||
"default_title": "WakaTime"
|
||||
},
|
||||
"browser_specific_settings": {},
|
||||
"description": "Automatic time tracking for Chrome.",
|
||||
"devtools_page": "devtools.html",
|
||||
"homepage_url": "https://wakatime.com",
|
||||
"icons": {
|
||||
"16": "graphics/wakatime-logo-16.png",
|
||||
"48": "graphics/wakatime-logo-48.png",
|
||||
"128": "graphics/wakatime-logo-128.png"
|
||||
},
|
||||
"manifest_version": 2,
|
||||
"name": "WakaTime",
|
||||
"options_ui": {
|
||||
"chrome_style": false,
|
||||
"page": "options.html"
|
||||
},
|
||||
"permissions": [
|
||||
"https://api.wakatime.com/*",
|
||||
"https://wakatime.com/*",
|
||||
"alarms",
|
||||
"tabs",
|
||||
"storage",
|
||||
"idle"
|
||||
],
|
||||
"version": "2.0.1"
|
||||
}
|
||||
28
src/types/heartbeats.d.ts
vendored
Normal file
28
src/types/heartbeats.d.ts
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
// Generated by https://quicktype.io
|
||||
|
||||
export interface HeartBeatsPayload {
|
||||
data: Datum[];
|
||||
end: string;
|
||||
start: string;
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
export interface Datum {
|
||||
branch: string;
|
||||
category: string;
|
||||
created_at: string;
|
||||
cursorpos: null;
|
||||
dependencies: string;
|
||||
entity: string;
|
||||
id: string;
|
||||
is_write: boolean;
|
||||
language: string;
|
||||
lineno: null;
|
||||
lines: number;
|
||||
machine_name_id: string;
|
||||
project: string;
|
||||
time: number;
|
||||
type: string;
|
||||
user_agent_id: string;
|
||||
user_id: string;
|
||||
}
|
||||
47
src/types/summaries.d.ts
vendored
Normal file
47
src/types/summaries.d.ts
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
// Generated by https://quicktype.io
|
||||
|
||||
export interface SummariesPayload {
|
||||
data: Datum[];
|
||||
end: string;
|
||||
start: string;
|
||||
}
|
||||
|
||||
export interface Datum {
|
||||
categories: Category[];
|
||||
dependencies: Category[];
|
||||
editors: Category[];
|
||||
grand_total: GrandTotal;
|
||||
languages: Category[];
|
||||
machines: Category[];
|
||||
operating_systems: Category[];
|
||||
projects: Category[];
|
||||
range: Range;
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
digital: string;
|
||||
hours: number;
|
||||
machine_name_id?: string;
|
||||
minutes: number;
|
||||
name: string;
|
||||
percent: number;
|
||||
seconds: number;
|
||||
text: string;
|
||||
total_seconds: number;
|
||||
}
|
||||
|
||||
export interface GrandTotal {
|
||||
digital: string;
|
||||
hours: number;
|
||||
minutes: number;
|
||||
text: string;
|
||||
total_seconds: number;
|
||||
}
|
||||
|
||||
export interface Range {
|
||||
date: string;
|
||||
end: string;
|
||||
start: string;
|
||||
text: string;
|
||||
timezone: string;
|
||||
}
|
||||
44
src/types/user.d.ts
vendored
Normal file
44
src/types/user.d.ts
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
// Generated by https://quicktype.io
|
||||
|
||||
export interface UserPayload {
|
||||
data: User;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
bio: null;
|
||||
color_scheme: string;
|
||||
created_at: string;
|
||||
date_format: string;
|
||||
default_dashboard_range: string;
|
||||
display_name: string;
|
||||
email: string;
|
||||
full_name: string;
|
||||
has_premium_features: boolean;
|
||||
human_readable_website: string;
|
||||
id: string;
|
||||
is_email_confirmed: boolean;
|
||||
is_email_public: boolean;
|
||||
is_hireable: boolean;
|
||||
is_onboarding_finished: boolean;
|
||||
languages_used_public: boolean;
|
||||
last_heartbeat_at: string;
|
||||
last_plugin: string;
|
||||
last_plugin_name: string;
|
||||
last_project: string;
|
||||
location: string;
|
||||
logged_time_public: boolean;
|
||||
modified_at: string;
|
||||
needs_payment_method: boolean;
|
||||
photo: string;
|
||||
photo_public: boolean;
|
||||
plan: string;
|
||||
public_email: string;
|
||||
show_machine_name_ip: boolean;
|
||||
time_format_24hr: boolean;
|
||||
timeout: number;
|
||||
timezone: string;
|
||||
username: string;
|
||||
website: string;
|
||||
weekday_start: number;
|
||||
writes_only: boolean;
|
||||
}
|
||||
28
src/utils/changeExtensionIcon.ts
Normal file
28
src/utils/changeExtensionIcon.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { browser } from 'webextension-polyfill-ts';
|
||||
import config from '../config';
|
||||
|
||||
type ColorIconTypes = 'gray' | 'red' | 'white' | '';
|
||||
|
||||
/**
|
||||
* It changes the extension icon color.
|
||||
*/
|
||||
export default async function changeExtensionIcon(color?: ColorIconTypes): Promise<void> {
|
||||
if (color) {
|
||||
const path = `./graphics/wakatime-logo-38-${color}.png`;
|
||||
|
||||
await browser.browserAction.setIcon({
|
||||
path: path,
|
||||
});
|
||||
} else {
|
||||
const { theme } = await browser.storage.sync.get({
|
||||
theme: config.theme,
|
||||
});
|
||||
const path =
|
||||
theme === config.theme
|
||||
? './graphics/wakatime-logo-38.png'
|
||||
: './graphics/wakatime-logo-38-white.png';
|
||||
await browser.browserAction.setIcon({
|
||||
path: path,
|
||||
});
|
||||
}
|
||||
}
|
||||
34
src/utils/changeExtensionState.ts
Normal file
34
src/utils/changeExtensionState.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import config, { ApiStates } from '../config';
|
||||
|
||||
import changeExtensionIcon from './changeExtensionIcon';
|
||||
import changeExtensionTooltip from './changeExtensionTooltip';
|
||||
|
||||
/**
|
||||
* Sets the current state of the extension.
|
||||
*/
|
||||
export default async function changeExtensionState(state: ApiStates): Promise<void> {
|
||||
switch (state) {
|
||||
case 'allGood':
|
||||
await changeExtensionIcon(config.colors.allGood);
|
||||
await changeExtensionTooltip(config.tooltips.allGood);
|
||||
break;
|
||||
case 'notLogging':
|
||||
await changeExtensionIcon(config.colors.notLogging);
|
||||
await changeExtensionTooltip(config.tooltips.notLogging);
|
||||
break;
|
||||
case 'notSignedIn':
|
||||
await changeExtensionIcon(config.colors.notSignedIn);
|
||||
await changeExtensionTooltip(config.tooltips.notSignedIn);
|
||||
break;
|
||||
case 'blacklisted':
|
||||
await changeExtensionIcon(config.colors.notLogging);
|
||||
await changeExtensionTooltip(config.tooltips.blacklisted);
|
||||
break;
|
||||
case 'whitelisted':
|
||||
await changeExtensionIcon(config.colors.notLogging);
|
||||
await changeExtensionTooltip(config.tooltips.whitelisted);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
16
src/utils/changeExtensionTooltip.ts
Normal file
16
src/utils/changeExtensionTooltip.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { browser } from 'webextension-polyfill-ts';
|
||||
import config from '../config';
|
||||
|
||||
/**
|
||||
* It changes the extension title
|
||||
*
|
||||
*/
|
||||
export default async function changeExtensionTooltip(text: string): Promise<void> {
|
||||
if (text === '') {
|
||||
text = config.name;
|
||||
} else {
|
||||
text = `${config.name} - ${text}`;
|
||||
}
|
||||
|
||||
await browser.browserAction.setTitle({ title: text });
|
||||
}
|
||||
24
src/utils/contains.ts
Normal file
24
src/utils/contains.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Creates an array from list using \n as delimiter
|
||||
* and checks if any element in list is contained in the url.
|
||||
*/
|
||||
export default function contains(url: string, list: string): boolean {
|
||||
const lines = list.split('\n');
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
// Trim all lines from the list one by one
|
||||
const cleanLine = lines[i].trim();
|
||||
|
||||
// If by any chance one line in the list is empty, ignore it
|
||||
if (cleanLine === '') continue;
|
||||
|
||||
const lineRe = new RegExp(cleanLine.replace('.', '.').replace('*', '.*'));
|
||||
|
||||
// If url matches the current line return true
|
||||
if (lineRe.test(url)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
8
src/utils/getDomainFromUrl.ts
Normal file
8
src/utils/getDomainFromUrl.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Returns domain from given URL.
|
||||
*/
|
||||
export function getDomainFromUrl(url: string): string {
|
||||
const parts = url.split('/');
|
||||
|
||||
return parts[0] + '//' + parts[2];
|
||||
}
|
||||
12
src/utils/inArray.ts
Normal file
12
src/utils/inArray.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Returns boolean if needle is found in haystack or not.
|
||||
*/
|
||||
export function in_array<T>(needle: T, haystack: T[]): boolean {
|
||||
for (let i = 0; i < haystack.length; i++) {
|
||||
if (needle == haystack[i]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user