;
+};
+export default function TimeEntriesList (props: TimeEntriesListProps) {
+ const { timeEntries = [], projects = {} } = props;
+ const dayGroups = getTimeEntryDayGroups(timeEntries);
+ // let renderedEntriesCount = 0;
+ console.log(dayGroups);
+ return (
+
+ {Object.keys(dayGroups).map((date) => {
+ const groupEntries = dayGroups[date];
+ return [
+ {format(date, 'ddd, D MMM')},
+ ...groupEntries.map((timeEntry, i) => {
+ const project = projects[timeEntry.pid] || null;
+ return ;
+ })
+ ]
+ })}
+
+ );
+}
+
+type TimeEntriesListItemProps = {
+ timeEntry: TimeEntry;
+ project: Project;
+ dataId: number;
+};
+function TimeEntriesListItem ({ timeEntry, project, ...props }: TimeEntriesListItemProps) {
+ const description = timeEntry.description || NO_DESCRIPTION;
+ const isBillable = !!timeEntry.billable;
+ const tags = (timeEntry.tags && timeEntry.tags.length > 0)
+ ? timeEntry.tags.join(', ')
+ : '';
+
+ return (
+
+
+ {description}
+
+
+ {tags && }
+
+
+
+
+
+
+
+
+ );
+}
+
+function TimeEntryDuration ({ duration }: { duration: number }) {
+ if (!duration || duration < 0) return null;
+ return (
+ {secToDecimalHours(duration)}
+ );
+}
+
+const TimeEntryDescription = styled.div`
+ flex: 1;
+
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ cursor: text;
+ line-height: normal;
+ overflow: hidden;
+ color: #222;
+`;
+
+function TimeEntryProject ({ project }: { project: Project }) {
+ return (
+
+ {project &&
+
+ {project.name}
+
+ }
+
+ );
+}
+
+const ContinueButton = styled.div`
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ background: url(${play}) no-repeat;
+ background-position: 55% 50%;
+ background-size: 14px;
+ border: none;
+ cursor: pointer;
+ opacity: 0.5;
+
+ &:hover {
+ opacity: 1.0;
+ }
+`;
+
+const EntryList = styled.ul`
+ list-style: none;
+ white-space: nowrap;
+ padding: 0;
+ margin: 0;
+
+ background-color: ${color.white};
+ font-family: Roboto, Helvetica, Arial, sans-serif;
+`;
+const itemPadding = '.5rem';
+const itemShadow = 'rgb(232, 232, 232) 0px -1px 0px 0px inset';
+const EntryItem = styled.li`
+ display: flex;
+ flex-direction: column;
+
+ padding: ${itemPadding};
+ height: 50px;
+
+ color: ${color.grey};
+ font-size: 14px;
+ box-shadow: ${itemShadow};
+
+ &:hover {
+ background-color: ${color.listItemHover};
+ }
+
+ &:last-child {
+ box-shadow: none;
+ }
+
+ & ~ EntryHeading {
+ margin-top: 1rem;
+ }
+`;
+
+const EntryHeading = styled.div`
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+
+ padding: ${itemPadding};
+ height: 25px;
+ color: ${color.black};
+ font-weight: ${text.bold};
+ box-shadow: ${itemShadow};
+`;
+
+const EntryItemRow = styled.div`
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+
+ flex: 1;
+`;
+
+const EntryIcons = styled.div`
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+
+ > * {
+ margin: 0 .25rem;
+ }
+`;
diff --git a/src/scripts/components/play.svg b/src/scripts/components/play.svg
new file mode 100644
index 000000000..be2730100
--- /dev/null
+++ b/src/scripts/components/play.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/scripts/index.d.ts b/src/scripts/index.d.ts
new file mode 100644
index 000000000..7bf506fad
--- /dev/null
+++ b/src/scripts/index.d.ts
@@ -0,0 +1,26 @@
+type TimeEntry = {
+ id: number;
+ pid: number;
+
+ start: string;
+ stop: string;
+
+ description: string;
+ duration: number;
+ tags: Array | null;
+ billable: boolean;
+};
+
+type Project = {
+ id: number;
+ hex_color: string;
+ name: string;
+};
+
+type IdMap = {
+ [index: string]: T
+}
+declare module "*.svg" {
+ const content: any;
+ export default content;
+}
diff --git a/src/scripts/popup.js b/src/scripts/popup.js
index bc59c0788..c4a0cc2dd 100644
--- a/src/scripts/popup.js
+++ b/src/scripts/popup.js
@@ -1,3 +1,7 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+
+import TimeEntriesList from './components/TimeEntriesList';
import { ProjectAutoComplete, TagAutoComplete } from './lib/autocomplete';
const browser = require('webextension-polyfill');
@@ -219,129 +223,37 @@ window.PopUp = {
renderEntriesList: function () {
const html = document.createElement('div');
const entries = TogglButton.$user.time_entries;
- const listEntries = [];
- let visibleIcons = '';
- let joinedTags;
- let te;
- let iconDiv;
- let p;
- let pname;
- let pstyle;
- let elem;
- let li;
- let t;
- let b;
- let i;
- let count = 0;
-
if (!entries || entries.length < 1) {
return;
}
- const checkUnique = function (te, listEntries) {
- let j;
- if (listEntries.length > 0) {
- for (j = 0; j < listEntries.length; j++) {
- if (
- listEntries[j].description === te.description &&
- listEntries[j].pid === te.pid
- ) {
- return false;
- }
- }
- }
- listEntries.push(te);
- return te;
- };
-
- const ul = document.createElement('ul');
+ const { listEntries, projects } = [...entries].reverse().reduce((sum, entry) => {
+ if (sum.listEntries.length >= 5) return sum;
- for (i = entries.length - 1; i >= 0; i--) {
- if (count >= 5) {
- break;
+ const exists = sum.listEntries.some((te) => te.description === entry.description && te.pid === entry.pid);
+ if (!exists) {
+ sum.listEntries.push(entry);
}
- te = checkUnique(entries[i], listEntries);
- if (!!te && te.duration >= 0) {
- visibleIcons = '';
- p = TogglButton.findProjectByPid(te.pid);
- if (p) {
- pname = p.name;
- pstyle = 'background-color: ' + p.hex_color + ';';
- p = document.createElement('div');
- p.className = 'tb-project-bullet tb-project-color';
- p.setAttribute('style', pstyle);
- } else {
- p = false;
- }
-
- t = !!te.tags && te.tags.length;
- joinedTags = t ? te.tags.join(', ') : '';
-
- t = t ? 'tag-icon-visible' : '';
- b = te.billable ? 'billable-icon-visible' : '';
- visibleIcons = t + ' ' + b;
-
- li = document.createElement('li');
- li.setAttribute('data-id', i);
-
- // Description
- elem = document.createElement('div');
- elem.className = 'te-desc';
- elem.setAttribute('title', te.description);
- elem.textContent = te.description || '(no description)';
- li.appendChild(elem);
-
- // Project bullet and name
- elem = document.createElement('div');
- elem.className = 'te-proj';
- if (p) {
- elem.appendChild(p);
- elem.appendChild(document.createTextNode(pname));
- }
- li.appendChild(elem);
-
- // Continue Button
- elem = document.createElement('div');
- elem.className = 'te-continue';
- elem.textContent = 'Continue';
- li.appendChild(elem);
-
- // Icons
- elem = document.createElement('div');
- elem.className = 'te-icons ' + visibleIcons;
-
- iconDiv = document.createElement('div');
- iconDiv.className = 'tag-icon';
- iconDiv.setAttribute('title', joinedTags);
- elem.appendChild(iconDiv);
-
- iconDiv = document.createElement('div');
- iconDiv.className = 'billable-icon';
- iconDiv.setAttribute('title', 'billable');
- elem.appendChild(iconDiv);
- li.appendChild(elem);
-
- ul.appendChild(li);
- count++;
+ const project = TogglButton.findProjectByPid(entry.pid);
+ if (project) {
+ sum.projects[project.id] = project;
}
- }
- if (count === 0) {
+ return sum;
+ }, { listEntries: [], projects: {} });
+
+ if (!listEntries.length) {
return;
}
- elem = document.createElement('p');
- elem.textContent = 'Recent entries';
- elem.addEventListener('click', e => e.stopPropagation());
- html.appendChild(elem);
-
- html.appendChild(ul);
-
// Remove old html
while (PopUp.$entries.firstChild) {
PopUp.$entries.removeChild(PopUp.$entries.firstChild);
}
+ // Render react tree
+ html.id = 'root-time-entries-list';
PopUp.$entries.appendChild(html);
+ ReactDOM.render(, document.getElementById('root-time-entries-list'));
},
setupIcons: function (data) {
@@ -674,7 +586,10 @@ document.addEventListener('DOMContentLoaded', function () {
});
PopUp.$entries.addEventListener('click', function (e) {
- const id = e.target.closest('[data-id]').getAttribute('data-id');
+ if (!e.target.dataset.continueId) {
+ return;
+ }
+ const id = e.target.dataset.continueId;
const timeEntry = TogglButton.$user.time_entries[id];
const request = {
diff --git a/src/styles/popup.css b/src/styles/popup.css
index 73f6644ae..3a415773e 100644
--- a/src/styles/popup.css
+++ b/src/styles/popup.css
@@ -158,7 +158,7 @@ p.form-row.first {
list-style: none;
margin: 0;
padding: 5px 10px;
- min-width: 220px;
+ min-width: 350px;
}
.header li {
float: left;
@@ -283,28 +283,13 @@ p.form-row.first {
}
#menu {
- background: #fff;
+ background: #fafbfc; /* colors.offWhite */
list-style: none;
margin: 0;
- padding: 6px 0;
+ padding: 0;
min-width: 100%;
font-weight: 400;
}
-#menu li {
- white-space: nowrap;
- list-style: none;
- height: 29px;
-}
-#menu li:not(.has-resume):not(.entries-list):hover {
- background-color: #e6e6e6;
- color: #222;
-}
-
-#menu li.has-resume .stop-button:hover,
-#menu li.has-resume .resume-button:hover {
- background-color: #e6e6e6;
- color: #222;
-}
#menu li > button {
width: 100%;
@@ -798,89 +783,6 @@ hr {
text-align: right;
}
-/** Time Entries list **/
-
-#menu .entries-list {
- height: auto;
- font-size: 12px;
-}
-
-.entries-list p {
- color: #a3a3a3;
- font-weight: 200;
- padding-left: 5px;
- font-size: 13px;
- text-align: center;
-}
-
-.entries-list ul {
- margin-top: 15px;
- margin-left: 0;
- padding-left: 0;
-}
-
-#menu .entries-list ul li {
- border-top: 1px solid #eceded;
- height: 45px;
- width: 100%;
- float: left;
- position: relative;
-}
-
-#menu .entries-list ul li .te-desc,
-#menu .entries-list ul li .te-proj {
- float: left;
- position: absolute;
- left: 10px;
- right: 40px;
- overflow: hidden;
- text-overflow: ellipsis;
- font-weight: 200;
-}
-
-#menu .entries-list ul li .te-desc {
- top: 5px;
-}
-
-#menu .entries-list ul li .te-proj {
- bottom: 5px;
- color: #a3a3a3;
- padding-left: 13px;
-}
-
-#menu .entries-list ul li .te-icons {
- float: right;
- position: relative;
-}
-
-#menu .entries-list ul li .te-icons > div {
- padding-top: 15px;
-}
-
-#menu .entries-list .tb-project-bullet {
- margin: 4px 0 3px -13px;
-}
-
-#menu .te-continue {
- background-color: #4bc800;
- width: 0;
- transition: width 200ms ease-in-out;
- height: 45px;
- line-height: 45px;
- color: #fff;
- text-align: center;
- float: right;
- position: relative;
- cursor: pointer;
-}
-
-#menu .entries-list ul li:hover .te-continue {
- width: 65px;
-}
-
-#menu .entries-list ul li:hover .te-icons {
- background-color: #e6e6e6;
-}
/** Revoked Workspace **/
#revoked-workspace {
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 000000000..1bf5a75e3
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "target": "es2016",
+ "module": "es6",
+ "moduleResolution": "node",
+ "isolatedModules": false,
+ "experimentalDecorators": true,
+ "emitDecoratorMetadata": true,
+ "declaration": false,
+ "noImplicitAny": false,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "alwaysStrict": true,
+ "strictNullChecks": true,
+ "removeComments": true,
+ "noLib": false,
+ "preserveConstEnums": true,
+ "sourceMap": true,
+ "suppressImplicitAnyIndexErrors": true,
+ "outDir": "dist",
+ "jsx": "react",
+ "jsxFactory": "React.createElement"
+ },
+ "compileOnSave": false,
+ "buildOnSave": false,
+ "exclude": [
+ "node_modules"
+ ]
+}
diff --git a/webpack.config.js b/webpack.config.js
index 9db7d2117..89f6df311 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -43,12 +43,24 @@ module.exports = config(async ({ development, bugsnagApiKey, production, release
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
},
+ resolve: {
+ extensions: ['.tsx', '.ts', '.js', '.jsx']
+ },
module: {
rules: [
{
- test: /\.js$/,
+ test: /\.tsx?$/,
+ exclude: /node_modules/,
+ use: 'ts-loader'
+ },
+ {
+ test: /\.jsx?$/,
exclude: /node_modules/,
use: 'babel-loader'
+ },
+ {
+ test: /\.svg$/,
+ loader: 'svg-url-loader'
}
]
},