From 093c78bf41d756c8f99a05468e329ba388fcbe99 Mon Sep 17 00:00:00 2001 From: Ben Thayer Date: Mon, 20 May 2024 01:21:08 +1000 Subject: [PATCH 1/6] Add time entry, incl start with create --- cmds/addTimeEntry.mjs | 87 +++++++++++++++++++++++++++++++++++++++++ cmds/index.mjs | 2 + cmds/startTimeEntry.mjs | 40 ++++++++++++++++++- utils.js | 14 +++++-- 4 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 cmds/addTimeEntry.mjs diff --git a/cmds/addTimeEntry.mjs b/cmds/addTimeEntry.mjs new file mode 100644 index 0000000..3fed9b0 --- /dev/null +++ b/cmds/addTimeEntry.mjs @@ -0,0 +1,87 @@ +/* eslint-disable no-unused-expressions */ +import Client from '../client.js' +import { defaultWorkspaceId, getProjectByName, getProjectById, appName, displayTimeEntry } from '../utils.js' +import dayjs from 'dayjs' +import debugClient from 'debug' +import utc from 'dayjs/plugin/utc.js' +import timezone from 'dayjs/plugin/timezone.js' +import yargs from 'yargs' +dayjs.extend(utc) +dayjs.extend(timezone) + +const debug = debugClient('toggl-cli-edit'); + +export const command = 'add ' +export const desc = 'Create a time entry. Time must be parsable by dayjs, e.g. 4:50PM or \'12:00 AM\'.' + +export const builder = { + d: { alias: ['description'], describe: 'Time entry name', type: 'string:' }, + p: { alias: ['projectId', 'project'], describe: 'The case insensitive project name or project id.', type: 'string', demandOption: false }, + s: { alias: ['start', 'startTime'], describe: 'The start time for the task, e.g. 13:00 12:45AM.', type: 'string', demandOption: false }, + e: { alias: ['end', 'endTime'], describe: 'The end time for the task, e.g. 13:00 12:45AM.', type: 'string', demandOption: false } +} + +export const handler = async function (argv) { + const client = await Client() + const params = {} + + params.workspace_id = +defaultWorkspaceId + let project + if (argv.projectId) { + if (isNaN(argv.projectId)) { + project = await getProjectByName(params.workspace_id, argv.projectId) + } else { + project = await getProjectById(params.workspace_id, argv.projectId) + } + } + + let startTime, endTime + if (dayjs(argv.startTime).isValid()) { + startTime = argv.startTime + } else { + // Parse the time and set it based upon the current time + startTime = parseTime(argv.startTime) + } + + if (dayjs(argv.endTime).isValid()) { + endTime = argv.endTime + } else { + // Parse the time and set it based upon the current time + endTime = parseTime(argv.endTime) + } + + params.created_with = appName + project ? params.project_id = +project.id : undefined + startTime ? params.start = startTime.toISOString() : undefined + endTime ? params.stop = endTime.toISOString() : undefined + if (startTime || endTime) { + const startTimeUnix = (startTime || dayjs(currentTimeEntry.start)).unix() + const endTimeUnix = (endTime || dayjs(currentTimeEntry.end) || dayjs()).unix() + let duration = endTimeUnix - startTimeUnix + duration = endTime ? duration : startTimeUnix * -1 + params.duration = duration + } + argv.description ? params.description = argv.description : undefined + debug(params) + const timeEntry = await client.timeEntries.create(params) + await displayTimeEntry(timeEntry) +} + +/** + * Parses a timelike string into a dayjs object of the current date and that time + * @param {string} timeString timelike string e.g. 4:50PM '12:00 AM' etc. + * @returns {object} dayjs object + */ +function parseTime (timeString) { + let h, m + // Assumes time in format 4:50 PM + const time = timeString.split(':', 2) + h = time[0] + m = time[1].match(/[0-9]+/)[0] + if (timeString.match(/PM/i) && h <= 12) { + // + in front of string converts to a number, cool! + h = +h + 12 + } + return dayjs().hour(h).minute(m).second(0) +} + diff --git a/cmds/index.mjs b/cmds/index.mjs index 1634922..43f5fcc 100644 --- a/cmds/index.mjs +++ b/cmds/index.mjs @@ -8,6 +8,7 @@ import * as edit from './edit.mjs' import * as web from './web.mjs' import * as startTimeEntry from './startTimeEntry.mjs' import * as stopTimeEntry from './stopTimeEntry.mjs' +import * as addTimeEntry from './addTimeEntry.mjs' import * as removeTimeEntry from './removeTimeEntry.mjs' import * as today from './today.mjs' import * as weekly from './weekly.mjs' @@ -21,6 +22,7 @@ export const commands = [ projects, startTimeEntry, stopTimeEntry, + addTimeEntry, removeTimeEntry, today, web, diff --git a/cmds/startTimeEntry.mjs b/cmds/startTimeEntry.mjs index 2860d8d..fbec97a 100644 --- a/cmds/startTimeEntry.mjs +++ b/cmds/startTimeEntry.mjs @@ -1,4 +1,5 @@ import { defaultWorkspaceId,getProjectByName, createTimeEntry, getProjectById, defaultProjectId } from '../utils.js' +import dayjs from 'dayjs' export const command = 'start' export const desc = 'Starts a time entry' @@ -10,7 +11,8 @@ export const builder = { }, p: { alias: ['projectId', 'project'], describe: 'The case insensitive project name or project id.', type: 'string', demandOption: false }, // TODO default to default workspace - w: { alias: ['workspaceId', 'workspace'], describe: 'The case insensitive workspace name or workspace id.', type: 'number', demandOption: false } + w: { alias: ['workspaceId', 'workspace'], describe: 'The case insensitive workspace name or workspace id.', type: 'number', demandOption: false }, + s: { alias: ['start', 'startTime'], describe: 'The start time for the task, e.g. 13:00 12:45AM.', type: 'string', demandOption: false }, } export const handler = async function (argv) { @@ -31,8 +33,44 @@ export const handler = async function (argv) { } } + if (argv.startTime) { + let startTime; + console.log(argv) + if (dayjs(argv.startTime).isValid()) { + startTime = argv.startTime + } else { + // Parse the time and set it based upon the current time + startTime = parseTime(argv.startTime) + } + + params.start = startTime.toISOString() + params.duration = -1 + } + params.projectId = project?.id || defaultProjectId || null // TODO check for invalid projectId or catch the error when creating fails + console.log(params) const timeEntry = await createTimeEntry(params) + console.log(timeEntry) console.info(`Started ${timeEntry?.description} ${project?.name ? `for project ${project.name}` : 'without a project'}`) } + +/** + * Parses a timelike string into a dayjs object of the current date and that time + * @param {string} timeString timelike string e.g. 4:50PM '12:00 AM' etc. + * @returns {object} dayjs object + */ +function parseTime (timeString) { + let h, m + // Assumes time in format 4:50 PM + const time = timeString.split(':', 2) + h = time[0] + m = time[1].match(/[0-9]+/)[0] + if (timeString.match(/PM/i) && h <= 12) { + // + in front of string converts to a number, cool! + h = +h + 12 + } else if (h == 12) { + h = 0 + } + return dayjs().hour(h).minute(m).second(0).millisecond(0) +} diff --git a/utils.js b/utils.js index c4efbb0..a0b3bbf 100644 --- a/utils.js +++ b/utils.js @@ -53,7 +53,7 @@ export const createTimeEntry = async function (params) { description: params.description, workspace_id: +params.workspaceId, project_id: +params.projectId, - start: dayjs().toISOString(), + start: params.start ? params.start : dayjs().toISOString(), duration: -1 * dayjs().unix(), created_with: appName, at: dayjs().toISOString() @@ -114,9 +114,15 @@ export const displayTimeEntry = async function (timeEntry) { console.info(`Billable: ${chalk.gray(timeEntry.billable)}`) // TODO this should be abstracted for reuse - const startTime = dayjs.unix(timeEntry.duration * -1) - const duration = dayjs().diff(startTime, 's') - const durationFormatted = dayjs.duration(duration * 1000).format('H[h] m[m]') + let durationFormatted; + if (timeEntry.stop == null) { + const startTime = dayjs.unix(timeEntry.duration * -1) + const duration = dayjs().diff(startTime, 's') + durationFormatted = dayjs.duration(duration * 1000).format('H[h] m[m]') + } else { + const duration = dayjs(timeEntry.stop).diff(dayjs(timeEntry.start)) + durationFormatted = dayjs.duration(duration).format('H[h] m[m]') + } console.info(`Duration: ${chalk.green(durationFormatted)}`) From bbf2bb4b4b4ffb3ab3e58d1e17c219294bc78a7b Mon Sep 17 00:00:00 2001 From: Ben Thayer Date: Mon, 20 May 2024 01:26:39 +1000 Subject: [PATCH 2/6] Clean up code --- cmds/addTimeEntry.mjs | 20 +------------------- cmds/edit.mjs | 20 +------------------- cmds/startTimeEntry.mjs | 25 +------------------------ utils.js | 20 ++++++++++++++++++++ 4 files changed, 23 insertions(+), 62 deletions(-) diff --git a/cmds/addTimeEntry.mjs b/cmds/addTimeEntry.mjs index 3fed9b0..ce08cd2 100644 --- a/cmds/addTimeEntry.mjs +++ b/cmds/addTimeEntry.mjs @@ -1,6 +1,6 @@ /* eslint-disable no-unused-expressions */ import Client from '../client.js' -import { defaultWorkspaceId, getProjectByName, getProjectById, appName, displayTimeEntry } from '../utils.js' +import { defaultWorkspaceId, getProjectByName, getProjectById, appName, displayTimeEntry, parseTime } from '../utils.js' import dayjs from 'dayjs' import debugClient from 'debug' import utc from 'dayjs/plugin/utc.js' @@ -67,21 +67,3 @@ export const handler = async function (argv) { await displayTimeEntry(timeEntry) } -/** - * Parses a timelike string into a dayjs object of the current date and that time - * @param {string} timeString timelike string e.g. 4:50PM '12:00 AM' etc. - * @returns {object} dayjs object - */ -function parseTime (timeString) { - let h, m - // Assumes time in format 4:50 PM - const time = timeString.split(':', 2) - h = time[0] - m = time[1].match(/[0-9]+/)[0] - if (timeString.match(/PM/i) && h <= 12) { - // + in front of string converts to a number, cool! - h = +h + 12 - } - return dayjs().hour(h).minute(m).second(0) -} - diff --git a/cmds/edit.mjs b/cmds/edit.mjs index 6bf6958..180f012 100644 --- a/cmds/edit.mjs +++ b/cmds/edit.mjs @@ -1,6 +1,6 @@ /* eslint-disable no-unused-expressions */ import Client from '../client.js' -import { defaultWorkspaceId, getProjectByName, getProjectById, appName, displayTimeEntry } from '../utils.js' +import { defaultWorkspaceId, getProjectByName, getProjectById, appName, displayTimeEntry, parseTime } from '../utils.js' import dayjs from 'dayjs' import debugClient from 'debug' import utc from 'dayjs/plugin/utc.js' @@ -73,21 +73,3 @@ export const handler = async function (argv) { const timeEntry = await client.timeEntries.update(currentTimeEntry.id, params) await displayTimeEntry(timeEntry) } - -/** - * Parses a timelike string into a dayjs object of the current date and that time - * @param {string} timeString timelike string e.g. 4:50PM '12:00 AM' etc. - * @returns {object} dayjs object - */ -function parseTime (timeString) { - let h, m - // Assumes time in format 4:50 PM - const time = timeString.split(':', 2) - h = time[0] - m = time[1].match(/[0-9]+/)[0] - if (timeString.match(/PM/i) && h <= 12) { - // + in front of string converts to a number, cool! - h = +h + 12 - } - return dayjs().hour(h).minute(m).second(0) -} diff --git a/cmds/startTimeEntry.mjs b/cmds/startTimeEntry.mjs index fbec97a..e85dac4 100644 --- a/cmds/startTimeEntry.mjs +++ b/cmds/startTimeEntry.mjs @@ -1,4 +1,4 @@ -import { defaultWorkspaceId,getProjectByName, createTimeEntry, getProjectById, defaultProjectId } from '../utils.js' +import { defaultWorkspaceId, getProjectByName, createTimeEntry, getProjectById, defaultProjectId, parseTime } from '../utils.js' import dayjs from 'dayjs' export const command = 'start' @@ -35,7 +35,6 @@ export const handler = async function (argv) { if (argv.startTime) { let startTime; - console.log(argv) if (dayjs(argv.startTime).isValid()) { startTime = argv.startTime } else { @@ -49,28 +48,6 @@ export const handler = async function (argv) { params.projectId = project?.id || defaultProjectId || null // TODO check for invalid projectId or catch the error when creating fails - console.log(params) const timeEntry = await createTimeEntry(params) - console.log(timeEntry) console.info(`Started ${timeEntry?.description} ${project?.name ? `for project ${project.name}` : 'without a project'}`) } - -/** - * Parses a timelike string into a dayjs object of the current date and that time - * @param {string} timeString timelike string e.g. 4:50PM '12:00 AM' etc. - * @returns {object} dayjs object - */ -function parseTime (timeString) { - let h, m - // Assumes time in format 4:50 PM - const time = timeString.split(':', 2) - h = time[0] - m = time[1].match(/[0-9]+/)[0] - if (timeString.match(/PM/i) && h <= 12) { - // + in front of string converts to a number, cool! - h = +h + 12 - } else if (h == 12) { - h = 0 - } - return dayjs().hour(h).minute(m).second(0).millisecond(0) -} diff --git a/utils.js b/utils.js index a0b3bbf..4c53713 100644 --- a/utils.js +++ b/utils.js @@ -144,3 +144,23 @@ export const displayTimeEntry = async function (timeEntry) { console.info(`Workspace: ${workspace.name} (#${timeEntry.wid})`) } } + +/** + * Parses a timelike string into a dayjs object of the current date and that time + * @param {string} timeString timelike string e.g. 4:50PM '12:00 AM' etc. + * @returns {object} dayjs object + */ +export function parseTime (timeString) { + let h, m + // Assumes time in format 4:50 PM + const time = timeString.split(':', 2) + h = time[0] + m = time[1].match(/[0-9]+/)[0] + if (timeString.match(/PM/i) && h <= 12) { + // + in front of string converts to a number, cool! + h = +h + 12 + } else if (h == 12) { + h = 0 + } + return dayjs().hour(h).minute(m).second(0).millisecond(0) +} From fca55f578ff59d39c19679a90ad669c5b5ff299f Mon Sep 17 00:00:00 2001 From: Ben Thayer Date: Sat, 25 May 2024 15:25:19 +1000 Subject: [PATCH 3/6] Fixed bug displaying time of current time entry --- utils.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/utils.js b/utils.js index 4c53713..42dcf01 100644 --- a/utils.js +++ b/utils.js @@ -113,17 +113,15 @@ export const displayTimeEntry = async function (timeEntry) { console.info(`${chalk.blueBright(timeEntry.description ? timeEntry.description : 'no description')} ${chalk.yellow('#'+timeEntry.id)}`) console.info(`Billable: ${chalk.gray(timeEntry.billable)}`) - // TODO this should be abstracted for reuse - let durationFormatted; + let stopTime; if (timeEntry.stop == null) { - const startTime = dayjs.unix(timeEntry.duration * -1) - const duration = dayjs().diff(startTime, 's') - durationFormatted = dayjs.duration(duration * 1000).format('H[h] m[m]') + stopTime = dayjs() } else { - const duration = dayjs(timeEntry.stop).diff(dayjs(timeEntry.start)) - durationFormatted = dayjs.duration(duration).format('H[h] m[m]') + stopTime = dayjs(timeEntry.stop) } + const duration = stopTime.diff(dayjs(timeEntry.start)) + const durationFormatted = dayjs.duration(duration).format('H[h] m[m]') console.info(`Duration: ${chalk.green(durationFormatted)}`) const projects = await getProjects(timeEntry.wid) From 0956c931e8e08c7a687ac92b7253c3144e864a5b Mon Sep 17 00:00:00 2001 From: Beau Raines Date: Sat, 25 May 2024 11:31:05 -0700 Subject: [PATCH 4/6] minor tweak to debug --- cmds/addTimeEntry.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmds/addTimeEntry.mjs b/cmds/addTimeEntry.mjs index ce08cd2..824124d 100644 --- a/cmds/addTimeEntry.mjs +++ b/cmds/addTimeEntry.mjs @@ -9,7 +9,7 @@ import yargs from 'yargs' dayjs.extend(utc) dayjs.extend(timezone) -const debug = debugClient('toggl-cli-edit'); +const debug = debugClient('toggl-cli-add'); export const command = 'add ' export const desc = 'Create a time entry. Time must be parsable by dayjs, e.g. 4:50PM or \'12:00 AM\'.' From 2548f1fbd8308a29167c29b96c453e64579632e9 Mon Sep 17 00:00:00 2001 From: Beau Raines Date: Sat, 25 May 2024 12:37:29 -0700 Subject: [PATCH 5/6] works with both positional and option specified values Makes the positional `startTime`, `endTime` and `description` optional, so that they can _also_ be specified through cli options --- cmds/addTimeEntry.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmds/addTimeEntry.mjs b/cmds/addTimeEntry.mjs index 824124d..43724f3 100644 --- a/cmds/addTimeEntry.mjs +++ b/cmds/addTimeEntry.mjs @@ -11,11 +11,11 @@ dayjs.extend(timezone) const debug = debugClient('toggl-cli-add'); -export const command = 'add ' +export const command = 'add [startTime] [endTime] [description]' export const desc = 'Create a time entry. Time must be parsable by dayjs, e.g. 4:50PM or \'12:00 AM\'.' export const builder = { - d: { alias: ['description'], describe: 'Time entry name', type: 'string:' }, + d: { alias: ['description'], describe: 'Time entry name', type: 'string:', demandOption: true}, p: { alias: ['projectId', 'project'], describe: 'The case insensitive project name or project id.', type: 'string', demandOption: false }, s: { alias: ['start', 'startTime'], describe: 'The start time for the task, e.g. 13:00 12:45AM.', type: 'string', demandOption: false }, e: { alias: ['end', 'endTime'], describe: 'The end time for the task, e.g. 13:00 12:45AM.', type: 'string', demandOption: false } From 3f9cd50d77f31b01409a490d9b5ccff4e32d76e3 Mon Sep 17 00:00:00 2001 From: Beau Raines Date: Sat, 25 May 2024 12:43:27 -0700 Subject: [PATCH 6/6] Removes unused variables and import --- cmds/addTimeEntry.mjs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cmds/addTimeEntry.mjs b/cmds/addTimeEntry.mjs index 43724f3..01fe8bd 100644 --- a/cmds/addTimeEntry.mjs +++ b/cmds/addTimeEntry.mjs @@ -5,7 +5,6 @@ import dayjs from 'dayjs' import debugClient from 'debug' import utc from 'dayjs/plugin/utc.js' import timezone from 'dayjs/plugin/timezone.js' -import yargs from 'yargs' dayjs.extend(utc) dayjs.extend(timezone) @@ -55,8 +54,8 @@ export const handler = async function (argv) { startTime ? params.start = startTime.toISOString() : undefined endTime ? params.stop = endTime.toISOString() : undefined if (startTime || endTime) { - const startTimeUnix = (startTime || dayjs(currentTimeEntry.start)).unix() - const endTimeUnix = (endTime || dayjs(currentTimeEntry.end) || dayjs()).unix() + const startTimeUnix = startTime.unix() + const endTimeUnix = endTime.unix() let duration = endTimeUnix - startTimeUnix duration = endTime ? duration : startTimeUnix * -1 params.duration = duration