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:
Vu Nguyen
2021-01-16 19:31:52 -06:00
committed by GitHub
parent 9ecc2144aa
commit d194bcfe60
21 changed files with 660 additions and 10 deletions

View 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
View 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>;
}

View 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>
`);
});
});

View 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>
);
}

View File

@@ -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
View 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
View 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
View 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
View 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;
}

View 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,
});
}
}

View 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;
}
}

View 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
View 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;
}

View 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
View 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;
}