Skip to content

Commit

Permalink
feat(start): add --interactive flag for start command
Browse files Browse the repository at this point in the history
  • Loading branch information
ulisesantana committed Dec 18, 2023
1 parent 012ea18 commit ef73332
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 51 deletions.
54 changes: 11 additions & 43 deletions src/commands/setup.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
/* eslint-disable camelcase */
import {input, select} from '@inquirer/prompts';
import {Command} from '@oclif/core'
import path from "node:path";

import {configFilename} from "../core";
import {FileSystemDataSource, TogglApi, UserInfo, http} from "../infrastructure/data-sources";
import {ConfigurationRepositoryImplementation} from "../infrastructure/repositories";
import {inputTimeEntryDescription, inputToken, selectProject, selectWorkspace} from "../infrastructure/ui";

export default class Setup extends Command {
static description = 'Setup your config for track.'
Expand All @@ -14,59 +13,28 @@ export default class Setup extends Command {
'<%= config.bin %> <%= command.id %>',
]

private static sortByName = (a: { name: string }, b: { name: string }) =>
a.name > b.name
? 1
: a.name < b.name
? -1
: 0

static async setApiKey(configurationRepository: ConfigurationRepositoryImplementation) {
const apiKey = await input({message: 'Enter your name API Key:\n(For getting your Toggl API token you can go to https://track.toggl.com/profile and scroll to the bottom of your profile)\n'});
const apiKey = await inputToken();
await configurationRepository.setApiToken(apiKey)
return apiKey;
}

static async setDefaultProject({clients, projects}: UserInfo, configurationRepository: ConfigurationRepositoryImplementation) {
const clientMap = new Map(clients.map(client => [client.id, client]))
const choices = projects
.filter(({active}) => active)
.sort(Setup.sortByName)
.map(project => ({
name: `${project.name} (${clientMap.get(project.client_id)?.name || 'No client'})`,
value: project.id
}))
const defaultProjectId = await select({
choices,
message: 'Select your default project',
});
await configurationRepository.setDefaultProjectId(defaultProjectId)
return choices.find(({value}) => value === defaultProjectId)?.name
static async setDefaultProject(userInfo: UserInfo, configurationRepository: ConfigurationRepositoryImplementation) {
const {id, name} = await selectProject(userInfo)
await configurationRepository.setDefaultProjectId(id)
return name
}

static async setDefaultTimeEntryDescription(configurationRepository: ConfigurationRepositoryImplementation) {
const defaultTimeEntry = await input({
default: 'Working',
message: 'Enter your default time entry description.',
validate: Boolean
});
const defaultTimeEntry = await inputTimeEntryDescription()
await configurationRepository.setDefaultTimeEntry(defaultTimeEntry)
return defaultTimeEntry
}

static async setDefaultWorkspace({default_workspace_id, workspaces}: UserInfo, configurationRepository: ConfigurationRepositoryImplementation) {
workspaces.sort(Setup.sortByName)
const defaultWorkspace = workspaces.find(({id}) => id === default_workspace_id)!
const choices = [defaultWorkspace, ...workspaces.filter(({id}) => id !== default_workspace_id)].map(workspace => ({
name: workspace.name,
value: workspace.id
}))
const defaultWorkspaceId = await select({
choices,
message: 'Select your default workspace',
});
await configurationRepository.setDefaultWorkspaceId(defaultWorkspaceId)
return choices.find(({value}) => value === defaultWorkspaceId)?.name
static async setDefaultWorkspace(userInfo: UserInfo, configurationRepository: ConfigurationRepositoryImplementation) {
const {id, name} = await selectWorkspace(userInfo)
await configurationRepository.setDefaultWorkspaceId(id)
return name
}

public async run(): Promise<void> {
Expand Down
33 changes: 25 additions & 8 deletions src/commands/start.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {Args, Flags} from '@oclif/core'

import {StartTimeEntryUseCase} from "../application/cases";
import {configFilename} from "../core";
import {TimeEntry, configFilename} from "../core";
import {TogglApi, http} from "../infrastructure/data-sources";
import {ProjectRepositoryImplementation, TimeEntryRepositoryImplementation} from "../infrastructure/repositories";
import {TrackCommand} from "../infrastructure/track-command";
import {inputTimeEntryDescription, selectProject} from "../infrastructure/ui";

export default class Start extends TrackCommand {
static args = {
Expand All @@ -17,13 +18,34 @@ export default class Start extends TrackCommand {
]

static flags = {
interactive: Flags.boolean({char: 'i', description: 'Create time entry interactively'}),
project: Flags.string({char: 'p', description: 'Project ID or Project name'}),
}

printStartedEntry(entry: TimeEntry) {
this.log(`Started time entry "${entry.description}" for "${entry.project.name}" project.`)
}

public async run(): Promise<void> {
const config = await this.getConfig(configFilename)
const togglAPI = new TogglApi({
http, token: config.apiToken, workspaceId: config.workspaceId
})
const projectRepository = new ProjectRepositoryImplementation(togglAPI)
const timeEntryRepository = new TimeEntryRepositoryImplementation(togglAPI)
const useCase = new StartTimeEntryUseCase(timeEntryRepository, projectRepository)

const {args, flags} = await this.parse(Start)
if (flags.interactive) {
const description = await inputTimeEntryDescription('What are you going to do?')
const userInfo = await TogglApi.getUserInfo(config.apiToken, http)
const project = await selectProject(userInfo, 'For which project?')

const newEntry = await useCase.exec({description, project: project.id})
this.printStartedEntry(newEntry)
return
}

if (!config.defaultTimeEntry && !args.description) {
this.error('Missing time entry description argument. ' +
'You can add a default time entry description with ' +
Expand All @@ -36,18 +58,13 @@ export default class Start extends TrackCommand {
' \'track set project\'.')
}

const togglAPI = new TogglApi({
http, token: config.apiToken, workspaceId: config.workspaceId
})
const projectRepository = new ProjectRepositoryImplementation(togglAPI)
const timeEntryRepository = new TimeEntryRepositoryImplementation(togglAPI)
const useCase = new StartTimeEntryUseCase(timeEntryRepository, projectRepository)

const description = args.description || config.defaultTimeEntry
const project = flags.project || config.projectId

try {
const newEntry = await useCase.exec({description, project})
this.log(`Started time entry "${newEntry.description}" for "${newEntry.project.name}" project.`)
this.printStartedEntry(newEntry)
} catch (error) {
this.error(`Unexpected error: ${error}`)
}
Expand Down
9 changes: 9 additions & 0 deletions src/core/validators/messages.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
export const messages = {
forms: {
description: {
default: 'Working',
message: 'Enter your default time entry description.'
},
project: 'Select your default project',
token: 'Enter your name API Key:\n(For getting your Toggl API token you can go to https://track.toggl.com/profile and scroll to the bottom of your profile)\n',
workspace: 'Select your default workspace'
},
missingConfig: {
token: `Your Toggl API key is missing in your config. Please, set your Toggl API token with 'track set token'.
Expand Down
5 changes: 5 additions & 0 deletions src/infrastructure/ui/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './input-time-entry-description'
export * from './input-token'
export * from './select-project'
export * from './select-workspace'

11 changes: 11 additions & 0 deletions src/infrastructure/ui/input-time-entry-description.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {input} from "@inquirer/prompts";

import {messages} from "../../core";

export function inputTimeEntryDescription(message = messages.forms.description.message) {
return input({
default: messages.forms.description.default,
message,
validate: Boolean
});
}
7 changes: 7 additions & 0 deletions src/infrastructure/ui/input-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {input} from "@inquirer/prompts";

import {messages} from "../../core";

export function inputToken() {
return input({message: messages.forms.token})
}
29 changes: 29 additions & 0 deletions src/infrastructure/ui/select-project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {select} from "@inquirer/prompts";

import {messages} from "../../core";
import {UserInfo} from "../data-sources";

const sortByName = (a: { name: string }, b: { name: string }) =>
a.name > b.name
? 1
: a.name < b.name
? -1
: 0
export async function selectProject({clients, projects}: UserInfo, message = messages.forms.project) {
const clientMap = new Map(clients.map(client => [client.id, client]))
const choices = projects
.filter(({active}) => active)
.sort(sortByName)
.map(project => ({
name: `${project.name} (${clientMap.get(project.client_id)?.name || 'No client'})`,
value: project.id
}))
const defaultProjectId = await select({
choices,
message,
});
return {
id: defaultProjectId,
name: choices.find(({value}) => value === defaultProjectId)?.name
}
}
29 changes: 29 additions & 0 deletions src/infrastructure/ui/select-workspace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* eslint-disable camelcase */
import {select} from "@inquirer/prompts";

import {messages} from "../../core";
import {UserInfo} from "../data-sources";

const sortByName = (a: { name: string }, b: { name: string }) =>
a.name > b.name
? 1
: a.name < b.name
? -1
: 0

export async function selectWorkspace({default_workspace_id, workspaces}: UserInfo) {
workspaces.sort(sortByName)
const defaultWorkspace = workspaces.find(({id}) => id === default_workspace_id)!
const choices = [defaultWorkspace, ...workspaces.filter(({id}) => id !== default_workspace_id)].map(workspace => ({
name: workspace.name,
value: workspace.id
}))
const defaultWorkspaceId = await select({
choices,
message: messages.forms.workspace,
});
return {
id: defaultWorkspaceId,
name: choices.find(({value}) => value === defaultWorkspaceId)?.name
}
}

0 comments on commit ef73332

Please sign in to comment.