Compare commits
12 Commits
8cd5a65542
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a7821a7cb5 | |||
| b9060b683f | |||
| 34a2814c19 | |||
|
|
2f9fa1cb0b | ||
| 0e8d7ffa32 | |||
| 168d938435 | |||
| 6b8b0a9097 | |||
| 8faa0443a5 | |||
| 2e885a890d | |||
| 96e1d0f10a | |||
| 3aa54c4474 | |||
| c62a74a635 |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,6 +1,6 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: daniep01 # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
github: [daniep01, krombel] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
|
||||
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
||||
lint-staged
|
||||
@@ -1,3 +1,3 @@
|
||||
# companion-module-toggl-track
|
||||
|
||||
See [HELP.md](./HELP.md) and [LICENSE](./LICENSE)
|
||||
See [HELP.md](./companion/HELP.md) and [LICENSE](./LICENSE)
|
||||
|
||||
@@ -18,7 +18,7 @@ Start a new timer running with the description set in the action and store the I
|
||||
|
||||
**Get Current Timer**
|
||||
|
||||
Companion only knows the ID of timers it has started, if a timer is started from another application or the toggle website then this action will get the ID so Companion knows about it.
|
||||
Companion only knows the ID of timers it has started. If you did not enable time entry poller you can use this action to poll the current time entry, if a timer is started from another application or the toggle website so Companion knows about it.
|
||||
|
||||
**Stop Current Timer**
|
||||
|
||||
@@ -36,32 +36,46 @@ Presets are available for **Start Timer** and **Stop Timer**.
|
||||
|
||||
### Version 1.0.0
|
||||
|
||||
First release
|
||||
- First release
|
||||
|
||||
### Version 1.0.1
|
||||
|
||||
Fix broken link
|
||||
- Fix broken link
|
||||
|
||||
### Version 1.0.2
|
||||
|
||||
Allow a project to be specified when starting a new timer button
|
||||
|
||||
Add an action to refresh the project list
|
||||
|
||||
Add 'Always start' configuration option
|
||||
- Allow a project to be specified when starting a new timer button
|
||||
- Add an action to refresh the project list
|
||||
- Add 'Always start' configuration option
|
||||
|
||||
### Version 1.0.3
|
||||
|
||||
Add variables for timerId and timerDescription
|
||||
- Add variables for timerId and timerDescription
|
||||
|
||||
### Version 2.0.0
|
||||
|
||||
Updated for Companion version 3
|
||||
|
||||
Updated for toggl API version 9
|
||||
- Updated for Companion version 3
|
||||
- Updated for toggl API version 9
|
||||
|
||||
### Version 2.0.1
|
||||
|
||||
Make the API token config field required
|
||||
- Make the API token config field required
|
||||
- Fix manifest file
|
||||
|
||||
Fix manifest file
|
||||
### Version 2.1.0
|
||||
|
||||
- Rewrite module in typescript
|
||||
- Use module toggl-track instead of implementing api on our own
|
||||
- Add status reports for some failure cases in connections dashboard
|
||||
- Add configurable time entry poller
|
||||
- Add feedback for currently running project and client
|
||||
- Update timerDuration to contain the correct duration formatted as time string
|
||||
|
||||
### Version 2.1.1
|
||||
|
||||
- Prevent module crash if user has no Clients
|
||||
|
||||
### Version 2.1.2
|
||||
|
||||
- Update node runtime to node22
|
||||
- make polling interval configurable as toggl is updating their [API usage limits](https://support.toggl.com/en/articles/11484112-api-webhook-limits)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "toggl-track",
|
||||
"shortname": "toggl-track",
|
||||
"description": "Companion module for toggltrack timers",
|
||||
"version": "2.0.1",
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"repository": "git+https://github.com/bitfocus/companion-module-toggl-track.git",
|
||||
"bugs": "https://github.com/bitfocus/companion-module-toggl-track/issues",
|
||||
@@ -17,7 +17,7 @@
|
||||
],
|
||||
"legacyIds": [],
|
||||
"runtime": {
|
||||
"type": "node18",
|
||||
"type": "node22",
|
||||
"api": "nodejs-ipc",
|
||||
"apiVersion": "0.0.0",
|
||||
"entrypoint": "../dist/main.js"
|
||||
|
||||
32
package.json
32
package.json
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"name": "toggl-track",
|
||||
"version": "2.1.0",
|
||||
"version": "2.1.2",
|
||||
"main": "dist/main.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"postinstall": "husky",
|
||||
"format": "prettier -w .",
|
||||
"package": "yarn run build && companion-module-build",
|
||||
"build": "rimraf dist && yarn run build:main",
|
||||
@@ -17,19 +18,27 @@
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/bitfocus/companion-module-toggl-track.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.14",
|
||||
"yarn": "^4"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@companion-module/base": "~1.11",
|
||||
"toggl-track": "^0.8.0"
|
||||
"@companion-module/base": "~1.11.3",
|
||||
"toggl-track": "^0.8.0",
|
||||
"ws": "^8.18.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@companion-module/tools": "~2.1.1",
|
||||
"@types/node": "^22.10.2",
|
||||
"eslint": "^9.17.0",
|
||||
"prettier": "^3.4.2",
|
||||
"rimraf": "^5.0.10",
|
||||
"typescript": "~5.5.4",
|
||||
"typescript-eslint": "^8.18.1"
|
||||
"@companion-module/tools": "~2.3.0",
|
||||
"@types/node": "^22.14.1",
|
||||
"@types/ws": "^8",
|
||||
"eslint": "^9.24.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^15.5.1",
|
||||
"prettier": "^3.5.3",
|
||||
"rimraf": "^6.0.1",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.30.1"
|
||||
},
|
||||
"prettier": "@companion-module/tools/.prettierrc.json",
|
||||
"lint-staged": {
|
||||
@@ -39,5 +48,6 @@
|
||||
"*.{ts,tsx,js,jsx}": [
|
||||
"yarn lint:raw --fix"
|
||||
]
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@4.9.1"
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function (self: TogglTrack): void {
|
||||
label: 'Project',
|
||||
id: 'project',
|
||||
default: '0',
|
||||
choices: self.projects!,
|
||||
choices: self.projects ?? [{ id: -1, label: 'None' }],
|
||||
},
|
||||
],
|
||||
callback: async ({ options }) => {
|
||||
@@ -44,7 +44,15 @@ export default function (self: TogglTrack): void {
|
||||
name: 'Refresh Project List',
|
||||
options: [],
|
||||
callback: async () => {
|
||||
await self.getWorkspace()
|
||||
await self.getProjects()
|
||||
},
|
||||
},
|
||||
|
||||
refreshStaticData: {
|
||||
name: 'Refresh Workspace, Project and Client List',
|
||||
options: [],
|
||||
callback: async () => {
|
||||
await self.loadStaticData()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -5,6 +5,9 @@ export interface ModuleConfig {
|
||||
workspaceName: string
|
||||
alwaysStart: boolean
|
||||
startTimerPoller: boolean
|
||||
timerPollerInterval: number
|
||||
startWebSocket: boolean
|
||||
websocketSecret: string
|
||||
}
|
||||
|
||||
export function GetConfigFields(): SomeCompanionConfigField[] {
|
||||
@@ -36,9 +39,32 @@ export function GetConfigFields(): SomeCompanionConfigField[] {
|
||||
{
|
||||
type: 'checkbox',
|
||||
id: 'startTimerPoller',
|
||||
label: 'Poll for current time entry every 30 seconds',
|
||||
label: 'Poll for current time entry every n seconds',
|
||||
width: 12,
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
id: 'timerPollerInterval',
|
||||
label: 'Poll interval in seconds',
|
||||
width: 12,
|
||||
default: 60,
|
||||
min: 30,
|
||||
max: 3600,
|
||||
},
|
||||
{
|
||||
type: 'checkbox',
|
||||
id: 'startWebSocket',
|
||||
label: 'start Websocket (krombel testing)',
|
||||
width: 12,
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
type: 'textinput',
|
||||
id: 'websocketSecret',
|
||||
label: 'secret to authenticate against Websocket server (krombel testing)',
|
||||
width: 12,
|
||||
default: 'secret',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
50
src/feedbacks.ts
Normal file
50
src/feedbacks.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { combineRgb } from '@companion-module/base'
|
||||
import type { TogglTrack } from './main.js'
|
||||
|
||||
export function UpdateFeedbacks(self: TogglTrack): void {
|
||||
self.setFeedbackDefinitions({
|
||||
ProjectRunningState: {
|
||||
name: 'Project Counting',
|
||||
type: 'boolean',
|
||||
defaultStyle: {
|
||||
bgcolor: combineRgb(255, 0, 0),
|
||||
color: combineRgb(0, 0, 0),
|
||||
},
|
||||
options: [
|
||||
{
|
||||
id: 'project',
|
||||
type: 'dropdown',
|
||||
label: 'Project',
|
||||
default: -1,
|
||||
choices: self.projects ?? [{ id: -1, label: 'None' }],
|
||||
},
|
||||
],
|
||||
callback: (feedback) => {
|
||||
//self.log('debug', 'check project counting ' + feedback.options.project)
|
||||
return feedback.options.project == self.currentEntry?.project_id
|
||||
},
|
||||
},
|
||||
ClientRunningState: {
|
||||
name: 'Client Counting',
|
||||
type: 'boolean',
|
||||
defaultStyle: {
|
||||
bgcolor: combineRgb(255, 0, 0),
|
||||
color: combineRgb(0, 0, 0),
|
||||
},
|
||||
options: [
|
||||
{
|
||||
id: 'client',
|
||||
type: 'dropdown',
|
||||
label: 'Client',
|
||||
default: -1,
|
||||
choices: self.clients ?? [{ id: -1, label: 'None' }],
|
||||
},
|
||||
],
|
||||
callback: (feedback) => {
|
||||
//self.log('debug', 'check client counting ' + feedback.options.client)
|
||||
// find the project that matches the project_id of the current entry and compare its client_id with the configured one
|
||||
return feedback.options.client == self.projects?.find((p) => p.id == self.currentEntry?.project_id)?.clientID
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
260
src/main.ts
260
src/main.ts
@@ -1,5 +1,5 @@
|
||||
// toggltrack module
|
||||
// Peter Daniel
|
||||
// Peter Daniel, Matthias Kesler
|
||||
|
||||
import { InstanceBase, runEntrypoint, InstanceStatus, SomeCompanionConfigField } from '@companion-module/base'
|
||||
import { GetConfigFields, type ModuleConfig } from './config.js'
|
||||
@@ -7,8 +7,11 @@ import UpdateActions from './actions.js'
|
||||
import UpdatePresets from './presets.js'
|
||||
import UpdateVariableDefinitions from './variables.js'
|
||||
import UpgradeScripts from './upgrades.js'
|
||||
import { Toggl, ITimeEntry, IWorkspaceProject } from 'toggl-track'
|
||||
import { togglGetWorkspaces } from './toggl-extend.js'
|
||||
import { UpdateFeedbacks } from './feedbacks.js'
|
||||
import { Toggl, ITimeEntry, IWorkspaceProject, IClient } from 'toggl-track'
|
||||
import { isITimeEntryWebhookPayload, togglGetWorkspaces } from './toggl-extend.js'
|
||||
import { timecodeSince } from './utils.js'
|
||||
import WebSocket from 'ws'
|
||||
|
||||
export class TogglTrack extends InstanceBase<ModuleConfig> {
|
||||
config!: ModuleConfig // Setup in init()
|
||||
@@ -17,8 +20,15 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
||||
|
||||
workspaceId?: number // current active workspace id
|
||||
workspaceName: string = '' // name of workspace
|
||||
projects?: { id: number; label: string }[]
|
||||
projects?: { id: number; label: string; clientID?: number }[]
|
||||
clients?: { id: number; label: string }[]
|
||||
currentEntry?: ITimeEntry
|
||||
|
||||
intervalId?: NodeJS.Timeout
|
||||
currentTimerUpdaterIntervalId?: NodeJS.Timeout
|
||||
wsReconnectTimer?: NodeJS.Timeout
|
||||
|
||||
ws?: WebSocket
|
||||
|
||||
constructor(internal: unknown) {
|
||||
super(internal)
|
||||
@@ -33,6 +43,12 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
||||
if (this.config.startTimerPoller) {
|
||||
this.stopTimeEntryPoller()
|
||||
}
|
||||
|
||||
if (this.config.startWebSocket) {
|
||||
this.stopWebSocket()
|
||||
}
|
||||
|
||||
clearInterval(this.currentTimerUpdaterIntervalId)
|
||||
}
|
||||
|
||||
async init(config: ModuleConfig): Promise<void> {
|
||||
@@ -40,30 +56,27 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
||||
|
||||
this.config = config
|
||||
|
||||
this.projects = [{ id: 0, label: 'None' }]
|
||||
|
||||
this.updateVariableDefinitions()
|
||||
this.updatePresets()
|
||||
|
||||
this.setVariableValues({
|
||||
timerId: undefined,
|
||||
timerDuration: undefined,
|
||||
timerDescription: undefined,
|
||||
lastTimerDuration: undefined,
|
||||
workspace: undefined,
|
||||
})
|
||||
|
||||
await this.initToggleConnection()
|
||||
|
||||
await this.loadStaticData()
|
||||
|
||||
this.updateActions()
|
||||
this.updateFeedbacks()
|
||||
|
||||
if (this.toggl && this.workspaceId) {
|
||||
this.updateStatus(InstanceStatus.Ok)
|
||||
}
|
||||
|
||||
await this.getCurrentTimer()
|
||||
if (this.config.startTimerPoller) {
|
||||
this.startTimeEntryPoller()
|
||||
}
|
||||
if (this.config.startWebSocket) {
|
||||
this.startWebSocket()
|
||||
}
|
||||
}
|
||||
|
||||
async configUpdated(config: ModuleConfig): Promise<void> {
|
||||
@@ -72,6 +85,7 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
||||
const apiTokenChanged: boolean = this.config.apiToken != config.apiToken
|
||||
const workSpaceDefaultChanged: boolean = this.config.workspaceName != config.workspaceName
|
||||
const timeEntryPollerChanged: boolean = this.config.startTimerPoller != config.startTimerPoller
|
||||
const webSocketChanged: boolean = this.config.startWebSocket != config.startWebSocket
|
||||
|
||||
this.config = config
|
||||
|
||||
@@ -79,9 +93,10 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
||||
this.log('debug', 'api token changed. init new toggle connection')
|
||||
this.toggl = undefined
|
||||
await this.initToggleConnection()
|
||||
await this.loadStaticData()
|
||||
} else if (workSpaceDefaultChanged) {
|
||||
this.log('debug', 'workspace default changed. reload workspaces')
|
||||
await this.getWorkspace()
|
||||
await this.loadStaticData()
|
||||
}
|
||||
|
||||
if (timeEntryPollerChanged) {
|
||||
@@ -91,6 +106,13 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
||||
this.stopTimeEntryPoller()
|
||||
}
|
||||
}
|
||||
if (webSocketChanged) {
|
||||
if (this.config.startWebSocket) {
|
||||
this.startWebSocket()
|
||||
} else {
|
||||
this.stopWebSocket()
|
||||
}
|
||||
}
|
||||
|
||||
this.updateActions()
|
||||
//this.updateVariables()
|
||||
@@ -105,6 +127,9 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
||||
updateActions(): void {
|
||||
UpdateActions(this)
|
||||
}
|
||||
updateFeedbacks(): void {
|
||||
UpdateFeedbacks(this)
|
||||
}
|
||||
|
||||
updatePresets(): void {
|
||||
UpdatePresets(this)
|
||||
@@ -129,8 +154,6 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
||||
this.updateStatus(InstanceStatus.AuthenticationFailure, resp)
|
||||
return
|
||||
}
|
||||
await this.getWorkspace()
|
||||
await this.getCurrentTimer()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,13 +165,131 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
||||
void (async () => {
|
||||
await this.getCurrentTimer()
|
||||
})()
|
||||
}, 30 * 1000)
|
||||
}, this.config.timerPollerInterval * 1000)
|
||||
}
|
||||
private stopTimeEntryPoller(): void {
|
||||
this.log('info', 'Stopping TimeEntry-Poller')
|
||||
clearInterval(this.intervalId)
|
||||
}
|
||||
|
||||
private startWebSocket(): void {
|
||||
this.log('info', 'start websocket')
|
||||
|
||||
this.updateStatus(InstanceStatus.Connecting)
|
||||
if (this.ws) {
|
||||
this.ws.close(1000)
|
||||
delete this.ws
|
||||
}
|
||||
|
||||
this.ws = new WebSocket('wss://doh.krombel.de/toggl-websocket')
|
||||
this.ws.addEventListener('open', () => {
|
||||
this.updateStatus(InstanceStatus.Ok)
|
||||
this.ws?.send(this.config.websocketSecret)
|
||||
})
|
||||
this.ws.addEventListener('close', (code) => {
|
||||
this.log('debug', `Connection closed with code ${code.code}`)
|
||||
this.updateStatus(InstanceStatus.Disconnected, `WS-Connection closed with code ${code.code}`)
|
||||
this.maybeReconnectWebSocket()
|
||||
})
|
||||
this.ws.addEventListener('message', this.websocketMessageHandler.bind(this))
|
||||
this.ws.addEventListener('error', (data) => {
|
||||
this.log('error', `WebSocket error: ${data.error}`)
|
||||
})
|
||||
}
|
||||
|
||||
private stopWebSocket(): void {
|
||||
this.log('info', 'stop websocket')
|
||||
|
||||
if (this.wsReconnectTimer) {
|
||||
clearTimeout(this.wsReconnectTimer)
|
||||
this.wsReconnectTimer = undefined
|
||||
}
|
||||
if (this.ws) {
|
||||
this.ws.close(1000)
|
||||
delete this.ws
|
||||
}
|
||||
}
|
||||
private maybeReconnectWebSocket() {
|
||||
if (this.wsReconnectTimer) {
|
||||
clearTimeout(this.wsReconnectTimer)
|
||||
}
|
||||
this.wsReconnectTimer = setTimeout(() => {
|
||||
this.startWebSocket()
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
private websocketMessageHandler(data: WebSocket.MessageEvent) {
|
||||
this.log('info', 'Websocket message received')
|
||||
|
||||
if (typeof data.data === 'string') {
|
||||
this.log('debug', `data: ${data.data}`)
|
||||
const event = JSON.parse(data.data)
|
||||
if (isITimeEntryWebhookPayload(event)) {
|
||||
const entry = event.payload
|
||||
if (entry.stop === '' && event.metadata.action != 'deleted') {
|
||||
this.log('info', `update time entry to ${entry.description}`)
|
||||
this.setCurrentlyRunningTimeEntry(entry)
|
||||
} else if (entry.id == this.currentEntry?.id) {
|
||||
// only unset value if update is for current entry
|
||||
this.log('info', 'stop time entry')
|
||||
this.setCurrentlyRunningTimeEntry(undefined)
|
||||
} else {
|
||||
this.log('warn', `unhandled case for time entry ${JSON.stringify(entry)}`)
|
||||
}
|
||||
} else {
|
||||
this.log('warn', `unhandled event ${data.data}`)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
this.log('warn', `unhandled websocket event '${JSON.stringify(data)}'`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set variables to this time entry
|
||||
* @param entry running entry or undefined
|
||||
*/
|
||||
private setCurrentlyRunningTimeEntry(entry: ITimeEntry | undefined): void {
|
||||
this.currentEntry = entry
|
||||
if (entry) {
|
||||
const project = this.projects?.find((p) => p.id == entry.project_id)
|
||||
this.setVariableValues({
|
||||
timerId: entry.id,
|
||||
timerDescription: entry.description,
|
||||
timerDuration: timecodeSince(new Date(entry.start)),
|
||||
timerProject: project?.label,
|
||||
timerProjectID: entry.project_id,
|
||||
timerClient: this.clients?.find((c) => c.id == project?.clientID)?.label ?? 'None',
|
||||
timerClientID: project?.clientID,
|
||||
})
|
||||
|
||||
// in case there is on update thread running clear it
|
||||
clearInterval(this.currentTimerUpdaterIntervalId)
|
||||
|
||||
// Update timerDuration once per second
|
||||
this.currentTimerUpdaterIntervalId = setInterval(() => {
|
||||
// this harms the linter (handle unawaited promise in an non-async context)
|
||||
void (async () => {
|
||||
this.setVariableValues({
|
||||
timerDuration: timecodeSince(new Date(entry.start)),
|
||||
})
|
||||
})()
|
||||
}, 1000) // update every second
|
||||
} else {
|
||||
clearInterval(this.currentTimerUpdaterIntervalId)
|
||||
this.setVariableValues({
|
||||
timerId: undefined,
|
||||
timerDescription: undefined,
|
||||
timerDuration: undefined,
|
||||
timerProject: undefined,
|
||||
timerProjectID: undefined,
|
||||
timerClient: undefined,
|
||||
timerClientID: undefined,
|
||||
})
|
||||
}
|
||||
this.checkFeedbacks('ProjectRunningState', 'ClientRunningState')
|
||||
}
|
||||
|
||||
async getCurrentTimer(): Promise<number | null> {
|
||||
this.log('debug', 'function: getCurrentTimer')
|
||||
|
||||
@@ -162,25 +303,27 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
||||
|
||||
if (entry) {
|
||||
this.log('info', 'Current timer id: ' + entry.id)
|
||||
this.setVariableValues({
|
||||
timerId: entry.id,
|
||||
timerDescription: entry.description,
|
||||
timerDuration: entry.duration,
|
||||
})
|
||||
this.setCurrentlyRunningTimeEntry(entry)
|
||||
|
||||
return entry.id
|
||||
} else {
|
||||
this.log('info', 'No current timer')
|
||||
this.setVariableValues({
|
||||
timerId: undefined,
|
||||
timerDescription: undefined,
|
||||
timerDuration: undefined,
|
||||
})
|
||||
this.setCurrentlyRunningTimeEntry(undefined)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async getWorkspace(): Promise<void> {
|
||||
async loadStaticData(): Promise<void> {
|
||||
if (!this.toggl) {
|
||||
this.log('warn', 'loadStaticData: toggle connection not set up')
|
||||
return
|
||||
}
|
||||
await this.getWorkspace()
|
||||
await this.getProjects()
|
||||
await this.getClients()
|
||||
}
|
||||
|
||||
private async getWorkspace(): Promise<void> {
|
||||
this.log('debug', 'function: getWorkspace')
|
||||
if (!this.toggl) {
|
||||
this.log('warn', 'Not authorized')
|
||||
@@ -210,7 +353,7 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
||||
if (this.workspaceId == undefined) {
|
||||
// no workspace found
|
||||
this.log('debug', 'workspace not found. Response: ' + JSON.stringify(workspaces))
|
||||
this.updateStatus(InstanceStatus.BadConfig, 'No workspace found')
|
||||
this.updateStatus(InstanceStatus.BadConfig, 'Available Workspaces: ' + workspaces.map((ws) => ws.name).join(','))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -220,6 +363,7 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
||||
})
|
||||
|
||||
await this.getProjects()
|
||||
await this.getClients()
|
||||
}
|
||||
|
||||
async getProjects(): Promise<void> {
|
||||
@@ -232,11 +376,9 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
||||
|
||||
const projects: IWorkspaceProject[] = await this.toggl!.projects.list(this.workspaceId)
|
||||
|
||||
//const projects: IWorkspaceProject[] = await togglGetProjects(this.toggl!, this.workspaceId!)
|
||||
|
||||
if (typeof projects === 'string' || projects.length == 0) {
|
||||
this.log('debug', 'No projects found')
|
||||
this.projects = [{ id: 0, label: 'None' }]
|
||||
this.projects = undefined
|
||||
this.log('debug', 'projects response' + JSON.stringify(projects))
|
||||
return
|
||||
}
|
||||
@@ -247,6 +389,7 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
||||
return {
|
||||
id: p.id,
|
||||
label: p.name,
|
||||
clientID: p.client_id,
|
||||
}
|
||||
})
|
||||
.sort((a, b) => {
|
||||
@@ -265,6 +408,47 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
||||
this.log('debug', 'Projects: ' + JSON.stringify(this.projects))
|
||||
}
|
||||
|
||||
private async getClients(): Promise<void> {
|
||||
this.log('debug', 'function: getClients ' + this.workspaceId)
|
||||
|
||||
if (!this.workspaceId) {
|
||||
this.log('warn', 'workspaceId undefined')
|
||||
return
|
||||
}
|
||||
|
||||
const clients: IClient[] = await this.toggl!.me.clients()
|
||||
|
||||
if (typeof clients === 'string' || clients.length == 0) {
|
||||
this.log('debug', 'No clients found')
|
||||
this.clients = undefined
|
||||
this.log('debug', 'clients response' + JSON.stringify(clients))
|
||||
return
|
||||
}
|
||||
|
||||
this.clients = clients
|
||||
.filter((c) => c.wid == this.workspaceId)
|
||||
.map((c) => {
|
||||
return {
|
||||
id: c.id,
|
||||
label: c.name,
|
||||
}
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const fa = a.label.toLowerCase()
|
||||
const fb = b.label.toLowerCase()
|
||||
|
||||
if (fa < fb) {
|
||||
return -1
|
||||
}
|
||||
if (fa > fb) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
this.log('debug', 'Clients: ' + JSON.stringify(this.clients))
|
||||
}
|
||||
|
||||
async startTimer(project: number, description: string): Promise<void> {
|
||||
if (!this.toggl || !this.workspaceId) {
|
||||
this.log('error', 'toggle not initialized. Do not start time')
|
||||
@@ -284,11 +468,7 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
||||
project_id: project != 0 ? project : undefined,
|
||||
})
|
||||
this.log('info', 'New timer started ' + newEntry.id + ' ' + newEntry.description)
|
||||
this.setVariableValues({
|
||||
timerId: newEntry.id,
|
||||
timerDescription: newEntry.description,
|
||||
timerDuration: newEntry.duration,
|
||||
})
|
||||
this.setCurrentlyRunningTimeEntry(newEntry)
|
||||
} else {
|
||||
this.log('info', 'A timer is already running ' + currentId + ' not starting a new one!')
|
||||
}
|
||||
@@ -308,10 +488,8 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
||||
const updated: ITimeEntry = await this.toggl.timeEntry.stop(currentId, this.workspaceId)
|
||||
this.log('info', 'Stopped ' + updated.id + ', duration ' + updated.duration)
|
||||
|
||||
this.setCurrentlyRunningTimeEntry(undefined)
|
||||
this.setVariableValues({
|
||||
timerId: undefined,
|
||||
timerDescription: undefined,
|
||||
timerDuration: undefined,
|
||||
lastTimerDuration: updated.duration,
|
||||
})
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Toggl } from 'toggl-track'
|
||||
import { ITimeEntry, Toggl } from 'toggl-track'
|
||||
|
||||
export interface IWorkspace {
|
||||
id: number
|
||||
@@ -9,3 +9,55 @@ export async function togglGetWorkspaces(toggl: Toggl): Promise<IWorkspace[]> {
|
||||
const resp: IWorkspace[] = await toggl.request<IWorkspace[]>('workspaces', {})
|
||||
return resp
|
||||
}
|
||||
|
||||
export interface IWebhookMetadata {
|
||||
action: 'created' | 'updated' | 'deleted'
|
||||
event_user_id: number
|
||||
entity_id: number
|
||||
model: 'time_entry'
|
||||
workspace_id: number
|
||||
project_id: number
|
||||
}
|
||||
|
||||
export interface ITimeEntryWebhookPayload {
|
||||
event_id: number
|
||||
created_at: string
|
||||
creator_id: number
|
||||
metadata: IWebhookMetadata
|
||||
payload: ITimeEntry
|
||||
subscription_id: number
|
||||
timestamp: string
|
||||
url_callback: string
|
||||
}
|
||||
|
||||
export function isITimeEntryWebhookPayload(event: unknown): event is ITimeEntryWebhookPayload {
|
||||
return (
|
||||
typeof event === 'object' &&
|
||||
event != null &&
|
||||
'metadata' in event &&
|
||||
typeof event.metadata === 'object' &&
|
||||
event.metadata !== null &&
|
||||
'model' in event.metadata &&
|
||||
typeof event.metadata.model === 'string' &&
|
||||
event.metadata.model === 'time_entry' &&
|
||||
'payload' in event &&
|
||||
typeof event.payload === 'object' &&
|
||||
event.payload !== null &&
|
||||
isITimeEntry(event.payload)
|
||||
)
|
||||
}
|
||||
|
||||
export function isITimeEntry(entry: unknown): entry is ITimeEntry {
|
||||
return (
|
||||
typeof entry === 'object' &&
|
||||
entry != null &&
|
||||
'id' in entry &&
|
||||
typeof entry.id === 'number' &&
|
||||
'description' in entry &&
|
||||
typeof entry.description === 'string' &&
|
||||
'project_id' in entry &&
|
||||
typeof entry.project_id === 'number' &&
|
||||
'start' in entry &&
|
||||
typeof entry.start === 'string'
|
||||
)
|
||||
}
|
||||
|
||||
12
src/utils.ts
Normal file
12
src/utils.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* returns formatted timecode from date until now
|
||||
* @param date start date
|
||||
* @returns formatted string 0:00:00 - where hours and minutes are hidden if 0
|
||||
*/
|
||||
export function timecodeSince(date: Date): string {
|
||||
const dateObj = new Date(Date.now() - date.getTime())
|
||||
const hours = dateObj.getUTCHours()
|
||||
const minutes = `0${dateObj.getUTCMinutes()}`.slice(-2)
|
||||
const seconds = `0${dateObj.getSeconds()}`.slice(-2)
|
||||
return (hours > 0 ? hours + ':' : '') + (hours > 0 || minutes !== '00' ? minutes + ':' : '') + seconds
|
||||
}
|
||||
@@ -22,5 +22,21 @@ export default function (self: TogglTrack): void {
|
||||
name: 'Current Timer Description',
|
||||
variableId: 'timerDescription',
|
||||
},
|
||||
{
|
||||
name: 'Current Timer Project ID',
|
||||
variableId: 'timerProjectID',
|
||||
},
|
||||
{
|
||||
name: 'Current Timer Project',
|
||||
variableId: 'timerProject',
|
||||
},
|
||||
{
|
||||
name: 'Current Timer Client ID',
|
||||
variableId: 'timerClientID',
|
||||
},
|
||||
{
|
||||
name: 'Current Timer Client',
|
||||
variableId: 'timerClient',
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user