qw/timer (#23)
* Rename logo and add 32x32 version * Set timer icon if a timer is running * Do not query activities on initialization * Show timer in bubble if timed activity exists * Pass timed activity to App * Code cleanup * Show timer view and stop timer * Make hours optional * Use booked seconds instead of hours * Add type submit to form button * Define colors as sass variables⎄ * Style timer view * Show start timer submit label * Update view layouts and content * Update version and changelog * Dyanically set iframe height * Reduce h1 font size * Add svg webpack loader * Parse empty string (TimeInputParser) * Forward ref in Popup component * Start time on current day only, format buttons * Improve styling * Set standard height as iframe default height, validate form * Upgrade packages to supress react warning * Show activity form in popup after timer was stoped * Use stop-watch icon in timer view * Fix empty description * Close TimerView if timer stopped for current service * Style timerview * Improve timer view styling * qw/setting-time-tracking-hh-mm (#24) * Format duration depending on settingTimeTrackingHHMM * Fix formatDuation without second argument * Fix time format after updating bubble * Add tests for formatDuration
This commit is contained in:
@@ -7,17 +7,21 @@ button.moco-bx-btn {
|
||||
white-space: nowrap;
|
||||
color: white;
|
||||
background-image: none;
|
||||
background-color: #7dc332;
|
||||
border-color: #7dc332;
|
||||
background-color: $green;
|
||||
border-color: $green;
|
||||
border-radius: 0;
|
||||
border-style: solid;
|
||||
box-shadow: none;
|
||||
font-size: 100%;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: #639a28;
|
||||
border-color: #639a28;
|
||||
background-color: $green-dark;
|
||||
border-color: $green-dark;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
@import "variables";
|
||||
@import "button";
|
||||
|
||||
input {
|
||||
@@ -15,7 +16,8 @@ input {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
input,
|
||||
textarea {
|
||||
padding: 6px 12px;
|
||||
background-color: white;
|
||||
border-color: #cccccc;
|
||||
@@ -31,13 +33,14 @@ input {
|
||||
}
|
||||
|
||||
&.has-error {
|
||||
input, textarea {
|
||||
border-color: #FB3A2F;
|
||||
input,
|
||||
textarea {
|
||||
border-color: $red;
|
||||
}
|
||||
}
|
||||
|
||||
.form-error {
|
||||
color: #FB3A2F;
|
||||
color: $red;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
@@ -71,8 +74,8 @@ input[name="hours"] {
|
||||
outline: 0 !important;
|
||||
|
||||
&:focus {
|
||||
border: 1px solid #38b5eb;
|
||||
box-shadow: 0 0 0 1px #38b5eb;
|
||||
border: 1px solid $blue;
|
||||
box-shadow: 0 0 0 1px $blue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,8 +87,7 @@ textarea[name="description"] {
|
||||
outline: 0 !important;
|
||||
|
||||
&:focus {
|
||||
border: 1px solid #38b5eb;
|
||||
box-shadow: 0 0 0 1px #38b5eb;
|
||||
border: 1px solid $blue;
|
||||
box-shadow: 0 0 0 1px $blue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
$font-family: Arial, sans-serif;
|
||||
$font-family: Roboto, Arial, sans-serif;
|
||||
$font-color: #191919;
|
||||
$popup-width: 420px;
|
||||
$popup-height: 463px;
|
||||
|
||||
$green: #7dc332;
|
||||
$green-dark: #639a28;
|
||||
$blue: #38b5eb;
|
||||
$red: #fb3a2f;
|
||||
$gray-base: #a3a3a3;
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
color: $font-color;
|
||||
pointer-events: all;
|
||||
|
||||
.text-red {
|
||||
color: $red;
|
||||
}
|
||||
|
||||
.moco-bx-bubble {
|
||||
box-sizing: content-box;
|
||||
position: fixed;
|
||||
|
||||
@@ -32,11 +32,11 @@
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #7DC332;
|
||||
color: $green;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #FB3A2F;
|
||||
color: $red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@import "variables";
|
||||
@import "form";
|
||||
@import "spinner";
|
||||
@import "variables";
|
||||
|
||||
html {
|
||||
overflow: hidden;
|
||||
@@ -14,33 +14,48 @@ html {
|
||||
#moco-bx-root {
|
||||
min-width: 516px;
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: normal;
|
||||
line-height: 1.5;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
line-height: 1.5;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.text-red {
|
||||
color: $red;
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: $gray-base;
|
||||
}
|
||||
|
||||
.moco-bx-app-container {
|
||||
width: 324px;
|
||||
padding: 3rem 6rem;
|
||||
|
||||
.moco-bx-logo__container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 3rem;
|
||||
text-align: center;
|
||||
|
||||
img.moco-bx-logo {
|
||||
flex: 0 0 48px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
||||
}
|
||||
|
||||
h1 {
|
||||
line-height: 48px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.moco-bx-calendar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 3rem;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
.moco-bx-calendar__day {
|
||||
display: flex;
|
||||
@@ -66,12 +81,11 @@ html {
|
||||
flex: 0 0 42px;
|
||||
color: white;
|
||||
background-color: #eee;
|
||||
|
||||
}
|
||||
|
||||
&.moco-bx-calendar__day--filled {
|
||||
.moco-bx-calendar__hours {
|
||||
background-color: #7dc332;
|
||||
background-color: $green;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,43 +98,73 @@ html {
|
||||
|
||||
&.moco-bx-calendar__day--active {
|
||||
.moco-bx-calendar__hours {
|
||||
background-color: #38b5eb;
|
||||
background-color: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.moco-bx-timer-view {
|
||||
text-align: center;
|
||||
margin-top: 3rem;
|
||||
|
||||
h2 {
|
||||
margin-top: 2rem;
|
||||
max-height: 90px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.2rem;
|
||||
}
|
||||
|
||||
span.moco-bx-single-line {
|
||||
display: inline-block;
|
||||
max-height: 19px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.timer {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.btn-stop-timer {
|
||||
margin-top: 1.5rem;
|
||||
background-color: $red;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.moco-bx-error-container {
|
||||
font-size: 18px;
|
||||
line-height: 1.5;
|
||||
width: 420px;
|
||||
padding: 3rem;
|
||||
text-align: center;
|
||||
|
||||
h1 {
|
||||
font-size: 35px;
|
||||
font-weight: normal;
|
||||
margin-top: 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
img {
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
&.moco-bx-logo {
|
||||
width: 48px;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
text-align: left;
|
||||
&.firefox-addons {
|
||||
margin-top: 0;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
src/images/icons/stopwatch-light.svg
Normal file
1
src/images/icons/stopwatch-light.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fal" data-icon="stopwatch" class="svg-inline--fa fa-stopwatch fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M393.3 141.3l17.5-17.5c4.7-4.7 4.7-12.3 0-17l-5.7-5.7c-4.7-4.7-12.3-4.7-17 0l-17.5 17.5c-35.8-31-81.5-50.9-131.7-54.2V32h25c6.6 0 12-5.4 12-12v-8c0-6.6-5.4-12-12-12h-80c-6.6 0-12 5.4-12 12v8c0 6.6 5.4 12 12 12h23v32.6C91.2 73.3 0 170 0 288c0 123.7 100.3 224 224 224s224-100.3 224-224c0-56.1-20.6-107.4-54.7-146.7zM224 480c-106.1 0-192-85.9-192-192S117.9 96 224 96s192 85.9 192 192-85.9 192-192 192zm4-128h-8c-6.6 0-12-5.4-12-12V172c0-6.6 5.4-12 12-12h8c6.6 0 12 5.4 12 12v168c0 6.6-5.4 12-12 12z"></path></svg>
|
||||
|
After Width: | Height: | Size: 733 B |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
BIN
src/images/moco-32x32.png
Normal file
BIN
src/images/moco-32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src/images/moco-timer-32x32.png
Normal file
BIN
src/images/moco-timer-32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
@@ -46,14 +46,16 @@ export default class Client {
|
||||
params: { date: `${formatDate(fromDate)}:${formatDate(toDate)}` },
|
||||
})
|
||||
|
||||
bookedHours = service => {
|
||||
activitiesStatus = service => {
|
||||
if (!service) {
|
||||
return Promise.resolve({ data: { hours: 0 } })
|
||||
}
|
||||
return this.#client.get("activities/tags", {
|
||||
params: { selection: [service.id], remote_service: service.name },
|
||||
return this.#client.get("activities/status", {
|
||||
params: { remote_id: service.id, remote_service: service.name },
|
||||
})
|
||||
}
|
||||
|
||||
createActivity = activity => this.#client.post("activities", { activity })
|
||||
|
||||
stopTimer = timedActivity => this.#client.get(`activities/${timedActivity.id}/stop_timer`)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,39 @@ import "@babel/polyfill"
|
||||
import ApiClient from "api/Client"
|
||||
import { isChrome, getCurrentTab, getSettings, isBrowserTab } from "utils/browser"
|
||||
import { BackgroundMessenger } from "utils/messaging"
|
||||
import { tabUpdated, settingsChanged, togglePopup } from "utils/messageHandlers"
|
||||
import { tabUpdated, settingsChanged, togglePopup, openPopup } from "utils/messageHandlers"
|
||||
import { isNil } from "lodash"
|
||||
|
||||
const messenger = new BackgroundMessenger()
|
||||
|
||||
function timerStoppedForCurrentService(service, timedActivity) {
|
||||
return timedActivity.service_id && timedActivity.service_id === service?.id
|
||||
}
|
||||
|
||||
function resetBubble({ tab, settings, service, timedActivity }) {
|
||||
const apiClient = new ApiClient(settings)
|
||||
apiClient
|
||||
.activitiesStatus(service)
|
||||
.then(({ data }) => {
|
||||
messenger.postMessage(tab, {
|
||||
type: "showBubble",
|
||||
payload: {
|
||||
bookedSeconds: data.seconds,
|
||||
timedActivity: data.timed_activity,
|
||||
settingTimeTrackingHHMM: settings.settingTimeTrackingHHMM,
|
||||
service,
|
||||
},
|
||||
})
|
||||
})
|
||||
.then(() => {
|
||||
if (isNil(timedActivity) || timerStoppedForCurrentService(service, timedActivity)) {
|
||||
messenger.postMessage(tab, { type: "closePopup" })
|
||||
} else {
|
||||
openPopup(tab, { service, messenger })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
messenger.on("togglePopup", () => {
|
||||
getCurrentTab().then(tab => {
|
||||
if (tab && !isBrowserTab(tab)) {
|
||||
@@ -31,18 +60,7 @@ chrome.runtime.onMessage.addListener(action => {
|
||||
const apiClient = new ApiClient(settings)
|
||||
apiClient
|
||||
.createActivity(activity)
|
||||
.then(() => {
|
||||
messenger.postMessage(tab, { type: "closePopup" })
|
||||
apiClient.bookedHours(service).then(({ data }) => {
|
||||
messenger.postMessage(tab, {
|
||||
type: "showBubble",
|
||||
payload: {
|
||||
bookedHours: parseFloat(data[0]?.hours) || 0,
|
||||
service,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
.then(() => resetBubble({ tab, settings, service }))
|
||||
.catch(error => {
|
||||
if (error.response?.status === 422) {
|
||||
chrome.runtime.sendMessage({
|
||||
@@ -55,6 +73,19 @@ chrome.runtime.onMessage.addListener(action => {
|
||||
})
|
||||
}
|
||||
|
||||
if (action.type === "stopTimer") {
|
||||
const { timedActivity, service } = action.payload
|
||||
getCurrentTab().then(tab => {
|
||||
getSettings().then(settings => {
|
||||
const apiClient = new ApiClient(settings)
|
||||
apiClient
|
||||
.stopTimer(timedActivity)
|
||||
.then(() => resetBubble({ tab, settings, service, timedActivity }))
|
||||
.catch(() => null)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (action.type === "openOptions") {
|
||||
let url
|
||||
if (isChrome()) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import PropTypes from "prop-types"
|
||||
import Spinner from "components/Spinner"
|
||||
import Form from "components/Form"
|
||||
import Calendar from "components/Calendar"
|
||||
import TimerView from "components/App/TimerView"
|
||||
import { observable, computed } from "mobx"
|
||||
import { Observer, observer } from "mobx-react"
|
||||
import { Spring, animated, config } from "react-spring/renderprops"
|
||||
@@ -41,9 +42,15 @@ class App extends Component {
|
||||
activities: PropTypes.array,
|
||||
schedules: PropTypes.array,
|
||||
projects: PropTypes.array,
|
||||
timedActivity: PropTypes.shape({
|
||||
customer_name: PropTypes.string.isRequired,
|
||||
assignment_name: PropTypes.string.isRequired,
|
||||
task_name: PropTypes.string.isRequired,
|
||||
timer_started_at: PropTypes.string.isRequired,
|
||||
seconds: PropTypes.number.isRequired,
|
||||
}),
|
||||
lastProjectId: PropTypes.number,
|
||||
lastTaskId: PropTypes.number,
|
||||
roundTimeEntries: PropTypes.bool,
|
||||
fromDate: PropTypes.string,
|
||||
toDate: PropTypes.string,
|
||||
errorType: PropTypes.string,
|
||||
@@ -54,7 +61,6 @@ class App extends Component {
|
||||
activities: [],
|
||||
schedules: [],
|
||||
projects: [],
|
||||
roundTimeEntries: false,
|
||||
}
|
||||
|
||||
@observable changeset = {}
|
||||
@@ -94,8 +100,8 @@ class App extends Component {
|
||||
task_id: this.task?.value,
|
||||
billable: this.billable,
|
||||
hours: "",
|
||||
seconds: this.changeset.hours && new TimeInputParser(this.changeset.hours).parseSeconds(),
|
||||
description: service?.description,
|
||||
seconds: new TimeInputParser(this.changeset.hours).parseSeconds(),
|
||||
description: service?.description || "",
|
||||
tag: "",
|
||||
}
|
||||
|
||||
@@ -104,6 +110,7 @@ class App extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener("keydown", this.handleKeyDown)
|
||||
parent.postMessage({ __mocoBX: { iFrameHeight: window.document.body.scrollHeight } }, "*")
|
||||
chrome.runtime.onMessage.addListener(this.handleSetFormErrors)
|
||||
}
|
||||
|
||||
@@ -130,6 +137,15 @@ class App extends Component {
|
||||
this.changeset.date = formatDate(date)
|
||||
}
|
||||
|
||||
handleStopTimer = timedActivity => {
|
||||
const { service } = this.props
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
type: "stopTimer",
|
||||
payload: { timedActivity, service },
|
||||
})
|
||||
}
|
||||
|
||||
handleSubmit = event => {
|
||||
event.preventDefault()
|
||||
const { service } = this.props
|
||||
@@ -161,6 +177,7 @@ class App extends Component {
|
||||
loading,
|
||||
subdomain,
|
||||
projects,
|
||||
timedActivity,
|
||||
activities,
|
||||
schedules,
|
||||
fromDate,
|
||||
@@ -191,25 +208,29 @@ class App extends Component {
|
||||
<animated.div className="moco-bx-app-container" style={props}>
|
||||
<Header subdomain={subdomain} />
|
||||
<Observer>
|
||||
{() => (
|
||||
<>
|
||||
<Calendar
|
||||
fromDate={parseISO(fromDate)}
|
||||
toDate={parseISO(toDate)}
|
||||
activities={activities}
|
||||
schedules={schedules}
|
||||
selectedDate={new Date(this.changesetWithDefaults.date)}
|
||||
onChange={this.handleSelectDate}
|
||||
/>
|
||||
<Form
|
||||
changeset={this.changesetWithDefaults}
|
||||
projects={projects}
|
||||
errors={this.formErrors}
|
||||
onChange={this.handleChange}
|
||||
onSubmit={this.handleSubmit}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{() =>
|
||||
timedActivity ? (
|
||||
<TimerView timedActivity={timedActivity} onStopTimer={this.handleStopTimer} />
|
||||
) : (
|
||||
<>
|
||||
<Calendar
|
||||
fromDate={parseISO(fromDate)}
|
||||
toDate={parseISO(toDate)}
|
||||
activities={activities}
|
||||
schedules={schedules}
|
||||
selectedDate={new Date(this.changesetWithDefaults.date)}
|
||||
onChange={this.handleSelectDate}
|
||||
/>
|
||||
<Form
|
||||
changeset={this.changesetWithDefaults}
|
||||
projects={projects}
|
||||
errors={this.formErrors}
|
||||
onChange={this.handleChange}
|
||||
onSubmit={this.handleSubmit}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</Observer>
|
||||
</animated.div>
|
||||
)}
|
||||
|
||||
45
src/js/components/App/TimerView.js
Normal file
45
src/js/components/App/TimerView.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import Timer from "components/shared/Timer"
|
||||
import { parseISO } from "date-fns"
|
||||
import StopWatch from "components/shared/StopWatch"
|
||||
|
||||
export default function TimerView({ timedActivity, onStopTimer }) {
|
||||
const handleStopTimer = () => {
|
||||
onStopTimer(timedActivity)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="moco-bx-timer-view">
|
||||
<p>
|
||||
<span className="moco-bx-single-line text-secondary">{timedActivity.customer_name}</span>
|
||||
<br />
|
||||
<span className="moco-bx-single-line">{timedActivity.assignment_name}</span>
|
||||
<br />
|
||||
<span className="moco-bx-single-line">{timedActivity.task_name}</span>
|
||||
</p>
|
||||
<h2>{timedActivity.description}</h2>
|
||||
<Timer
|
||||
className="timer text-red"
|
||||
startedAt={parseISO(timedActivity.timer_started_at)}
|
||||
offset={timedActivity.seconds}
|
||||
style={{ fontSize: "36px", display: "inline-block" }}
|
||||
/>
|
||||
<button className="moco-bx-btn btn-stop-timer" onClick={handleStopTimer}>
|
||||
<StopWatch />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
TimerView.propTypes = {
|
||||
timedActivity: PropTypes.shape({
|
||||
customer_name: PropTypes.string.isRequired,
|
||||
assignment_name: PropTypes.string.isRequired,
|
||||
task_name: PropTypes.string.isRequired,
|
||||
description: PropTypes.string,
|
||||
timer_started_at: PropTypes.string.isRequired,
|
||||
seconds: PropTypes.number.isRequired,
|
||||
}).isRequired,
|
||||
onStopTimer: PropTypes.func.isRequired,
|
||||
}
|
||||
@@ -1,22 +1,46 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import logoUrl from "images/logo.png"
|
||||
import mocoLogo from "images/moco-32x32.png"
|
||||
import mocoTimerLogo from "images/moco-timer-32x32.png"
|
||||
import { parseISO } from "date-fns"
|
||||
import { formatDuration } from "utils"
|
||||
import Timer from "./shared/Timer"
|
||||
|
||||
const Bubble = ({ bookedHours }) => (
|
||||
<div className="moco-bx-bubble-inner">
|
||||
<img className="moco-bx-logo" src={chrome.extension.getURL(logoUrl)} />
|
||||
{bookedHours > 0 ? (
|
||||
<span className="moco-bx-booked-hours">{bookedHours.toFixed(2)}</span>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
const Bubble = ({ bookedSeconds, timedActivity, settingTimeTrackingHHMM }) => {
|
||||
const logo = timedActivity ? mocoTimerLogo : mocoLogo
|
||||
|
||||
return (
|
||||
<div className="moco-bx-bubble-inner">
|
||||
<img className="moco-bx-logo" src={chrome.extension.getURL(logo)} />
|
||||
{!timedActivity && bookedSeconds > 0 && (
|
||||
<span className="moco-bx-booked-hours">
|
||||
{formatDuration(bookedSeconds, { settingTimeTrackingHHMM, showSeconds: false })}
|
||||
</span>
|
||||
)}
|
||||
{timedActivity && (
|
||||
<Timer
|
||||
className="text-red"
|
||||
startedAt={parseISO(timedActivity.timer_started_at)}
|
||||
offset={timedActivity.seconds}
|
||||
style={{ marginBottom: "3px", fontSize: "12px" }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Bubble.propTypes = {
|
||||
bookedHours: PropTypes.number,
|
||||
bookedSeconds: PropTypes.number,
|
||||
timedActivity: PropTypes.shape({
|
||||
timer_started_at: PropTypes.string.isRequired,
|
||||
seconds: PropTypes.number.isRequired,
|
||||
}),
|
||||
settingTimeTrackingHHMM: PropTypes.bool,
|
||||
}
|
||||
|
||||
Bubble.defaultProps = {
|
||||
bookedHours: 0,
|
||||
bookedSeconds: 0,
|
||||
settingTimeTrackingHHMM: false,
|
||||
}
|
||||
|
||||
export default Bubble
|
||||
|
||||
@@ -3,25 +3,23 @@ import settingsUrl from "images/settings.png"
|
||||
|
||||
const InvalidConfigurationError = () => (
|
||||
<div className="moco-bx-error-container">
|
||||
<h1>Bitte Einstellungen aktualisieren</h1>
|
||||
<ol>
|
||||
<li>Internetadresse eintragen</li>
|
||||
<li>Persönlichen API-Schlüssel eintragen</li>
|
||||
</ol>
|
||||
<h1>MOCO verbinden</h1>
|
||||
<p>
|
||||
Dazu trägst Du in den Einstellungen Deine Account-Internetadresse und Deinen API-Schlüssel
|
||||
ein.
|
||||
</p>
|
||||
<img
|
||||
src={chrome.extension.getURL(settingsUrl)}
|
||||
alt="Browser extension configuration settings"
|
||||
style={{ cursor: "pointer", width: "185px", height: "195px" }}
|
||||
onClick={() => chrome.runtime.sendMessage({ type: "openOptions" })}
|
||||
/>
|
||||
<button
|
||||
className="moco-bx-btn"
|
||||
onClick={() => chrome.runtime.sendMessage({ type: "openOptions" })}
|
||||
>
|
||||
Einstellungen öffnen
|
||||
Weiter zu den Einstellungen
|
||||
</button>
|
||||
<br />
|
||||
<br />
|
||||
<img
|
||||
src={chrome.extension.getURL(settingsUrl)}
|
||||
alt="Browser extension configuration settings"
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => chrome.runtime.sendMessage({ type: "openOptions" })}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import logo from "images/logo.png"
|
||||
import logo from "images/moco-159x159.png"
|
||||
|
||||
const UnknownError = ({ message = "Unbekannter Fehler" }) => (
|
||||
<div className="moco-bx-error-container">
|
||||
<img className="moco-bx-logo" src={logo} alt="MOCO logo" />
|
||||
<img
|
||||
className="moco-bx-logo"
|
||||
src={logo}
|
||||
style={{ width: "48px", height: "48px" }}
|
||||
alt="MOCO logo"
|
||||
/>
|
||||
<h1>Ups, es ist ein Fehler passiert!</h1>
|
||||
<p>Bitte überprüfe deine Internetverbindung.</p>
|
||||
<p>Wir wurden per Email benachrichtigt und untersuchen den Vorfall.</p>
|
||||
<br />
|
||||
<p>Fehlermeldung:</p>
|
||||
<pre>{message}</pre>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import React from "react"
|
||||
import { isChrome } from "utils/browser"
|
||||
import logo from "images/logo.png"
|
||||
import logo from "images/moco-159x159.png"
|
||||
import firefoxAddons from "images/firefox_addons.png"
|
||||
|
||||
const UpgradeRequiredError = () => (
|
||||
<div className="moco-bx-error-container">
|
||||
<img className="moco-bx-logo" src={logo} alt="MOCO logo" />
|
||||
<h1>Upgrade erforderlich</h1>
|
||||
<img
|
||||
className="moco-bx-logo"
|
||||
src={logo}
|
||||
style={{ width: "48px", height: "48px" }}
|
||||
alt="MOCO logo"
|
||||
/>
|
||||
<h1>Bitte aktualisieren</h1>
|
||||
<p>Die installierte MOCO Browser-Erweiterung ist veraltet — bitte aktualisieren.</p>
|
||||
{isChrome() ? (
|
||||
<button
|
||||
@@ -17,9 +22,13 @@ const UpgradeRequiredError = () => (
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<br />
|
||||
<p>Unter folgender URL:</p>
|
||||
<img className="firefox-addons" src={firefoxAddons} alt="about:addons" />
|
||||
<img
|
||||
className="firefox-addons"
|
||||
src={firefoxAddons}
|
||||
style={{ width: "292px", height: "40px" }}
|
||||
alt="about:addons"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
import React, { Component } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import Select from "components/Select"
|
||||
import { formatDate } from "utils"
|
||||
import cn from "classnames"
|
||||
import StopWatch from "components/shared/StopWatch"
|
||||
|
||||
class Form extends Component {
|
||||
static propTypes = {
|
||||
changeset: PropTypes.shape({
|
||||
project: PropTypes.object,
|
||||
task: PropTypes.object,
|
||||
assignment_id: PropTypes.number.isRequired,
|
||||
billable: PropTypes.bool.isRequired,
|
||||
date: PropTypes.string.isRequired,
|
||||
task_id: PropTypes.number.isRequired,
|
||||
description: PropTypes.string,
|
||||
remote_id: PropTypes.string,
|
||||
remote_service: PropTypes.string,
|
||||
remote_url: PropTypes.string,
|
||||
seconds: PropTypes.number,
|
||||
hours: PropTypes.string,
|
||||
}).isRequired,
|
||||
errors: PropTypes.object,
|
||||
@@ -20,9 +29,42 @@ class Form extends Component {
|
||||
inline: true,
|
||||
}
|
||||
|
||||
isValid = () => {
|
||||
isValid() {
|
||||
const { changeset } = this.props
|
||||
return ["assignment_id", "task_id", "hours"].map(prop => changeset[prop]).every(Boolean)
|
||||
return (
|
||||
["assignment_id", "task_id"].map(prop => changeset[prop]).every(Boolean) &&
|
||||
(changeset.date === formatDate(new Date()) || changeset.seconds > 0)
|
||||
)
|
||||
}
|
||||
|
||||
get isTimerStartable() {
|
||||
const {
|
||||
changeset: { seconds, date },
|
||||
} = this.props
|
||||
|
||||
return date === formatDate(new Date()) && seconds === 0
|
||||
}
|
||||
|
||||
buttonStyle() {
|
||||
const styleMap = {
|
||||
true: {
|
||||
border: "none",
|
||||
borderRadius: "50%",
|
||||
width: "60px",
|
||||
height: "60px",
|
||||
marginTop: "22px",
|
||||
transition: "all 0.2s ease-in-out",
|
||||
},
|
||||
false: {
|
||||
border: "none",
|
||||
width: "50px",
|
||||
height: "36px",
|
||||
marginTop: "35px",
|
||||
transition: "all 0.2s ease-in-out",
|
||||
},
|
||||
}
|
||||
|
||||
return styleMap[this.isTimerStartable]
|
||||
}
|
||||
|
||||
handleTextareaKeyDown = event => {
|
||||
@@ -96,8 +138,13 @@ class Form extends Component {
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<button className="moco-bx-btn" disabled={!this.isValid()}>
|
||||
OK
|
||||
<button
|
||||
type="submit"
|
||||
className="moco-bx-btn"
|
||||
disabled={!this.isValid()}
|
||||
style={this.buttonStyle()}
|
||||
>
|
||||
{this.isTimerStartable ? <StopWatch /> : "OK"}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
|
||||
@@ -25,7 +25,11 @@ class Options extends Component {
|
||||
handleSubmit = _event => {
|
||||
this.isSuccess = false
|
||||
this.errorMessage = null
|
||||
setStorage({ subdomain: this.subdomain, apiKey: this.apiKey }).then(() => {
|
||||
setStorage({
|
||||
subdomain: this.subdomain,
|
||||
apiKey: this.apiKey,
|
||||
settingTimeTrackingHHMM: false,
|
||||
}).then(() => {
|
||||
const { version } = chrome.runtime.getManifest()
|
||||
const apiClient = new ApiClient({
|
||||
subdomain: this.subdomain,
|
||||
@@ -34,6 +38,9 @@ class Options extends Component {
|
||||
})
|
||||
apiClient
|
||||
.login()
|
||||
.then(({ data }) =>
|
||||
setStorage({ settingTimeTrackingHHMM: data.setting_time_tracking_hh_mm }),
|
||||
)
|
||||
.then(() => {
|
||||
this.isSuccess = true
|
||||
this.closeWindow()
|
||||
|
||||
@@ -1,39 +1,24 @@
|
||||
import React, { Component } from "react"
|
||||
import React, { useEffect, useRef, forwardRef } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import queryString from "query-string"
|
||||
import { ERROR_UNKNOWN, ERROR_UNAUTHORIZED, ERROR_UPGRADE_REQUIRED, serializeProps } from "utils"
|
||||
import { isChrome } from "utils/browser"
|
||||
import { serializeProps } from "utils"
|
||||
|
||||
function getStyles(errorType) {
|
||||
return {
|
||||
width: "516px",
|
||||
height:
|
||||
errorType === ERROR_UNAUTHORIZED
|
||||
? "834px"
|
||||
: errorType === ERROR_UPGRADE_REQUIRED
|
||||
? isChrome()
|
||||
? "369px"
|
||||
: "461px"
|
||||
: errorType === ERROR_UNKNOWN
|
||||
? "550px"
|
||||
: "558px",
|
||||
}
|
||||
}
|
||||
const Popup = forwardRef((props, ref) => {
|
||||
const iFrameRef = useRef()
|
||||
|
||||
class Popup extends Component {
|
||||
static propTypes = {
|
||||
service: PropTypes.object,
|
||||
errorType: PropTypes.string,
|
||||
onRequestClose: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
handleRequestClose = event => {
|
||||
const handleRequestClose = event => {
|
||||
if (event.target.classList.contains("moco-bx-popup")) {
|
||||
this.props.onRequestClose()
|
||||
props.onRequestClose()
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const handleMessage = event => {
|
||||
if (iFrameRef.current && event.data?.__mocoBX?.iFrameHeight > 300) {
|
||||
iFrameRef.current.style.height = `${event.data.__mocoBX.iFrameHeight}px`
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Document might lose focus when clicking the browser action.
|
||||
// Document might be out of focus when hitting the shortcut key.
|
||||
// This puts the focus back to the document and ensures that:
|
||||
@@ -41,41 +26,47 @@ class Popup extends Component {
|
||||
// - the ESC key closes the popup without closing anything else
|
||||
window.focus()
|
||||
document.activeElement?.blur()
|
||||
}
|
||||
window.addEventListener("message", handleMessage)
|
||||
return () => {
|
||||
window.removeEventListener("message", handleMessage)
|
||||
}
|
||||
}, [])
|
||||
|
||||
render() {
|
||||
const serializedProps = serializeProps([
|
||||
"loading",
|
||||
"service",
|
||||
"subdomain",
|
||||
"lastProjectId",
|
||||
"lastTaskId",
|
||||
"roundTimeEntries",
|
||||
"projects",
|
||||
"activities",
|
||||
"schedules",
|
||||
"lastProjectId",
|
||||
"lastTaskId",
|
||||
"fromDate",
|
||||
"toDate",
|
||||
"errorType",
|
||||
"errorMessage",
|
||||
])(this.props)
|
||||
const serializedProps = serializeProps([
|
||||
"loading",
|
||||
"service",
|
||||
"subdomain",
|
||||
"projects",
|
||||
"activities",
|
||||
"schedules",
|
||||
"timedActivity",
|
||||
"lastProjectId",
|
||||
"lastTaskId",
|
||||
"fromDate",
|
||||
"toDate",
|
||||
"errorType",
|
||||
"errorMessage",
|
||||
])(props)
|
||||
|
||||
const styles = getStyles(this.props.errorType)
|
||||
|
||||
return (
|
||||
<div className="moco-bx-popup" onClick={this.handleRequestClose}>
|
||||
<div className="moco-bx-popup-content" style={styles}>
|
||||
<iframe
|
||||
src={chrome.extension.getURL(`popup.html?${queryString.stringify(serializedProps)}`)}
|
||||
width={styles.width}
|
||||
height={styles.height}
|
||||
/>
|
||||
</div>
|
||||
return (
|
||||
<div ref={ref} className="moco-bx-popup" onClick={handleRequestClose}>
|
||||
<div className="moco-bx-popup-content" style={{ width: "516px" }}>
|
||||
<iframe
|
||||
ref={iFrameRef}
|
||||
src={chrome.extension.getURL(`popup.html?${queryString.stringify(serializedProps)}`)}
|
||||
style={{ width: "516px", height: "576px", transition: "height 0.1s ease-in-out" }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
Popup.displayName = "Popup"
|
||||
|
||||
Popup.propTypes = {
|
||||
service: PropTypes.object,
|
||||
errorType: PropTypes.string,
|
||||
onRequestClose: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
export default Popup
|
||||
|
||||
@@ -36,7 +36,7 @@ const customTheme = theme => ({
|
||||
const customStyles = props => ({
|
||||
control: (base, _state) => ({
|
||||
...base,
|
||||
borderColor: props.hasError ? "#FB3A2F" : base.borderColor,
|
||||
borderColor: props.hasError ? "#fb3a2f" : base.borderColor,
|
||||
}),
|
||||
valueContainer: base => ({
|
||||
...base,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import logoUrl from "images/logo.png"
|
||||
import logoUrl from "images/moco-159x159.png"
|
||||
|
||||
const Header = ({ subdomain }) => (
|
||||
<div className="moco-bx-logo__container">
|
||||
|
||||
11
src/js/components/shared/StopWatch.js
Normal file
11
src/js/components/shared/StopWatch.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from "react"
|
||||
import stopWatch from "images/icons/stopwatch-light.svg"
|
||||
|
||||
export default function StopWatch() {
|
||||
return (
|
||||
<i
|
||||
dangerouslySetInnerHTML={{ __html: stopWatch }}
|
||||
style={{ width: "22px", color: "white", display: "inline-block" }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
29
src/js/components/shared/Timer.js
Normal file
29
src/js/components/shared/Timer.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import React, { useState } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { useInterval } from "./hooks"
|
||||
import { differenceInSeconds } from "date-fns"
|
||||
import { formatDuration } from "utils"
|
||||
|
||||
Timer.propTypes = {
|
||||
startedAt: PropTypes.instanceOf(Date).isRequired,
|
||||
offset: PropTypes.number,
|
||||
onTick: PropTypes.func,
|
||||
}
|
||||
|
||||
function Timer({ startedAt, offset = 0, onTick, ...domProps }) {
|
||||
const [timerLabel, setTimerLabel] = useState(formattedTimerLabel(startedAt, offset))
|
||||
|
||||
useInterval(() => {
|
||||
setTimerLabel(formattedTimerLabel(startedAt, offset))
|
||||
onTick && onTick()
|
||||
}, 1000)
|
||||
|
||||
return <span {...domProps}>{timerLabel}</span>
|
||||
}
|
||||
|
||||
function formattedTimerLabel(startedAt, offset) {
|
||||
const seconds = differenceInSeconds(new Date(), startedAt) + offset
|
||||
return formatDuration(seconds)
|
||||
}
|
||||
|
||||
export default Timer
|
||||
18
src/js/components/shared/hooks.js
Normal file
18
src/js/components/shared/hooks.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useEffect, useRef } from "react"
|
||||
|
||||
export function useInterval(callback, delay) {
|
||||
const savedCallback = useRef()
|
||||
|
||||
useEffect(() => {
|
||||
savedCallback.current = callback
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
function tick() {
|
||||
savedCallback.current()
|
||||
}
|
||||
|
||||
let id = setInterval(tick, delay)
|
||||
return () => clearInterval(id)
|
||||
}, [delay])
|
||||
}
|
||||
@@ -25,7 +25,7 @@ chrome.runtime.onConnect.addListener(function(port) {
|
||||
document.removeEventListener("click", clickHandler, true)
|
||||
})
|
||||
|
||||
function updateBubble({ service, bookedHours } = {}) {
|
||||
function updateBubble({ service, bookedSeconds, settingTimeTrackingHHMM, timedActivity } = {}) {
|
||||
if (!document.getElementById("moco-bx-root")) {
|
||||
const domRoot = document.createElement("div")
|
||||
domRoot.setAttribute("id", "moco-bx-root")
|
||||
@@ -47,7 +47,12 @@ chrome.runtime.onConnect.addListener(function(port) {
|
||||
// eslint-disable-next-line react/display-name
|
||||
(props => (
|
||||
<animated.div className="moco-bx-bubble" style={{ ...props, ...service.position }}>
|
||||
<Bubble key={service.url} bookedHours={bookedHours} />
|
||||
<Bubble
|
||||
key={service.url}
|
||||
bookedSeconds={bookedSeconds}
|
||||
settingTimeTrackingHHMM={settingTimeTrackingHHMM}
|
||||
timedActivity={timedActivity}
|
||||
/>
|
||||
</animated.div>
|
||||
))
|
||||
}
|
||||
@@ -86,8 +91,8 @@ chrome.runtime.onConnect.addListener(function(port) {
|
||||
})
|
||||
})
|
||||
|
||||
messenger.on("showBubble", ({ payload: { service, bookedHours } }) => {
|
||||
updateBubble({ service, bookedHours })
|
||||
messenger.on("showBubble", ({ payload }) => {
|
||||
updateBubble(payload)
|
||||
})
|
||||
|
||||
messenger.on("hideBubble", () => {
|
||||
@@ -101,8 +106,4 @@ chrome.runtime.onConnect.addListener(function(port) {
|
||||
messenger.on("closePopup", () => {
|
||||
closePopup()
|
||||
})
|
||||
|
||||
messenger.on("activityCreated", () => {
|
||||
closePopup()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -12,9 +12,7 @@ const parsedProps = parseProps([
|
||||
"projects",
|
||||
"activities",
|
||||
"schedules",
|
||||
"lastProjectId",
|
||||
"lastTaskId",
|
||||
"roundTimeEntries",
|
||||
"timedActivity",
|
||||
"lastProjectId",
|
||||
"lastTaskId",
|
||||
"fromDate",
|
||||
|
||||
@@ -2,11 +2,13 @@ export default class TimeInputParser {
|
||||
#input
|
||||
|
||||
constructor(input) {
|
||||
this.#input = input.toLowerCase().replace(/[\s()]/g, "")
|
||||
this.#input = (input ?? "").toLowerCase().replace(/[\s()]/g, "")
|
||||
}
|
||||
|
||||
parseSeconds() {
|
||||
if (this.#isDecimal()) {
|
||||
if (this.#input === "") {
|
||||
return 0
|
||||
} else if (this.#isDecimal()) {
|
||||
return Math.round(parseFloat(this.#parseDecimal()) * 3600)
|
||||
} else if (this.#isTime()) {
|
||||
return this.#parseTimeAsSeconds()
|
||||
|
||||
@@ -5,7 +5,7 @@ export const isFirefox = () => typeof browser !== "undefined" && chrome
|
||||
const DEFAULT_SUBDOMAIN = "unset"
|
||||
|
||||
export const getSettings = (withDefaultSubdomain = true) => {
|
||||
const keys = ["subdomain", "apiKey"]
|
||||
const keys = ["subdomain", "apiKey", "settingTimeTrackingHHMM"]
|
||||
const { version } = chrome.runtime.getManifest()
|
||||
if (isChrome()) {
|
||||
return new Promise(resolve => {
|
||||
|
||||
@@ -12,7 +12,9 @@ import {
|
||||
pick,
|
||||
head,
|
||||
defaultTo,
|
||||
padCharsStart,
|
||||
} from "lodash/fp"
|
||||
import { startOfWeek, endOfWeek } from "date-fns"
|
||||
import { format } from "date-fns"
|
||||
|
||||
const nilToArray = input => input || []
|
||||
@@ -105,6 +107,8 @@ export const trace = curry((tag, value) => {
|
||||
|
||||
export const weekStartsOn = 1
|
||||
export const formatDate = date => format(date, "yyyy-MM-dd")
|
||||
export const getStartOfWeek = () => startOfWeek(new Date(), { weekStartsOn })
|
||||
export const getEndOfWeek = () => endOfWeek(new Date(), { weekStartsOn })
|
||||
|
||||
export const extensionSettingsUrl = () => `chrome://extensions/?id=${chrome.runtime.id}`
|
||||
|
||||
@@ -120,3 +124,22 @@ export const extractAndSetTag = changeset => {
|
||||
tag: match[1],
|
||||
}
|
||||
}
|
||||
|
||||
export const formatDuration = (
|
||||
durationInSeconds,
|
||||
{ settingTimeTrackingHHMM = true, showSeconds = true } = {},
|
||||
) => {
|
||||
if (settingTimeTrackingHHMM) {
|
||||
const hours = Math.floor(durationInSeconds / 3600)
|
||||
const minutes = Math.floor((durationInSeconds % 3600) / 60)
|
||||
const result = `${hours}:${padCharsStart("0", 2, minutes)}`
|
||||
if (!showSeconds) {
|
||||
return result
|
||||
} else {
|
||||
const seconds = durationInSeconds % 60
|
||||
return result + `:${padCharsStart("0", 2, seconds)}`
|
||||
}
|
||||
} else {
|
||||
return (durationInSeconds / 3600).toFixed(2)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,34 +4,35 @@ import {
|
||||
ERROR_UPGRADE_REQUIRED,
|
||||
ERROR_UNKNOWN,
|
||||
groupedProjectOptions,
|
||||
weekStartsOn,
|
||||
getStartOfWeek,
|
||||
getEndOfWeek,
|
||||
} from "utils"
|
||||
import { get, forEach, reject, isNil } from "lodash/fp"
|
||||
import { startOfWeek, endOfWeek } from "date-fns"
|
||||
import { createMatcher } from "utils/urlMatcher"
|
||||
import remoteServices from "remoteServices"
|
||||
import { queryTabs, isBrowserTab, getSettings } from "utils/browser"
|
||||
import { queryTabs, isBrowserTab, getSettings, setStorage } from "utils/browser"
|
||||
|
||||
const getStartOfWeek = () => startOfWeek(new Date(), { weekStartsOn })
|
||||
const getEndOfWeek = () => endOfWeek(new Date(), { weekStartsOn })
|
||||
const matcher = createMatcher(remoteServices)
|
||||
|
||||
export function tabUpdated(tab, { messenger, settings }) {
|
||||
messenger.connectTab(tab)
|
||||
|
||||
const service = matcher(tab.url)
|
||||
const apiClient = new ApiClient(settings)
|
||||
|
||||
if (service?.match?.id) {
|
||||
messenger.postMessage(tab, { type: "requestService" })
|
||||
|
||||
messenger.once("newService", ({ payload: { service } }) => {
|
||||
const apiClient = new ApiClient(settings)
|
||||
apiClient
|
||||
.bookedHours(service)
|
||||
.activitiesStatus(service)
|
||||
.then(({ data }) => {
|
||||
messenger.postMessage(tab, {
|
||||
type: "showBubble",
|
||||
payload: {
|
||||
bookedHours: parseFloat(data[0]?.hours) || 0,
|
||||
bookedSeconds: data.seconds,
|
||||
settingTimeTrackingHHMM: settings.settingTimeTrackingHHMM,
|
||||
timedActivity: data.timed_activity,
|
||||
service,
|
||||
},
|
||||
})
|
||||
@@ -40,7 +41,8 @@ export function tabUpdated(tab, { messenger, settings }) {
|
||||
messenger.postMessage(tab, {
|
||||
type: "showBubble",
|
||||
payload: {
|
||||
bookedHours: 0,
|
||||
bookedSeconds: 0,
|
||||
settingTimeTrackingHHMM: settings.settingTimeTrackingHHMM,
|
||||
service,
|
||||
},
|
||||
})
|
||||
@@ -76,25 +78,36 @@ export function togglePopup(tab, { messenger }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function openPopup(tab, { service, messenger }) {
|
||||
export async function openPopup(tab, { service, messenger }) {
|
||||
messenger.postMessage(tab, { type: "openPopup", payload: { loading: true } })
|
||||
|
||||
const fromDate = getStartOfWeek()
|
||||
const toDate = getEndOfWeek()
|
||||
const settings = await getSettings()
|
||||
const apiClient = new ApiClient(settings)
|
||||
const responses = []
|
||||
try {
|
||||
const responses = await Promise.all([
|
||||
apiClient.login(service),
|
||||
apiClient.projects(),
|
||||
apiClient.activities(fromDate, toDate),
|
||||
apiClient.schedules(fromDate, toDate),
|
||||
])
|
||||
responses.push(await apiClient.login(service))
|
||||
// we can forgo the following calls if a timed activity exists
|
||||
if (!responses[0].data.timed_activity) {
|
||||
responses.push(
|
||||
...(await Promise.all([
|
||||
apiClient.projects(),
|
||||
apiClient.activities(fromDate, toDate),
|
||||
apiClient.schedules(fromDate, toDate),
|
||||
])),
|
||||
)
|
||||
}
|
||||
|
||||
const settingTimeTrackingHHMM = get("[0].data.setting_time_tracking_hh_mm", responses)
|
||||
!isNil(settingTimeTrackingHHMM) && setStorage({ settingTimeTrackingHHMM })
|
||||
|
||||
const action = {
|
||||
type: "openPopup",
|
||||
payload: {
|
||||
service,
|
||||
subdomain: settings.subdomain,
|
||||
timedActivity: get("[0].data.timed_activity", responses),
|
||||
lastProjectId: get("[0].data.last_project_id", responses),
|
||||
lastTaskId: get("[0].data.last_task_id", responses),
|
||||
roundTimeEntries: get("[0].data.round_time_entries", responses),
|
||||
|
||||
@@ -4,26 +4,21 @@
|
||||
"description": "MOCO Zeiterfassung Plugin",
|
||||
"version": "0.9.20",
|
||||
"manifest_version": 2,
|
||||
"description": "MOCO Time Tracking Plugin",
|
||||
"icons": {
|
||||
"16": "src/images/logo.png",
|
||||
"32": "src/images/logo.png",
|
||||
"48": "src/images/logo.png",
|
||||
"128": "src/images/logo.png"
|
||||
"16": "src/images/moco-32x32.png",
|
||||
"32": "src/images/moco-32x32.png",
|
||||
"48": "src/images/moco-159x159.png",
|
||||
"128": "src/images/moco-159x159.png"
|
||||
},
|
||||
"options_ui": {
|
||||
"page": "options.html"
|
||||
},
|
||||
"permissions": [
|
||||
"https://*.mocoapp.com/*",
|
||||
"storage",
|
||||
"tabs"
|
||||
],
|
||||
"permissions": ["https://*.mocoapp.com/*", "storage", "tabs"],
|
||||
"background": {
|
||||
"page": "background.html"
|
||||
},
|
||||
"browser_action": {
|
||||
"default_icon": "src/images/logo.png"
|
||||
"default_icon": "src/images/moco-32x32.png"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user