diff --git a/src/commands/setup.ts b/src/commands/setup.ts index b896d57..ef3aca4 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -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.' @@ -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 { diff --git a/src/commands/start.ts b/src/commands/start.ts index e483f8c..c3a56d6 100644 --- a/src/commands/start.ts +++ b/src/commands/start.ts @@ -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 = { @@ -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 { 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 ' + @@ -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}`) } diff --git a/src/core/validators/messages.ts b/src/core/validators/messages.ts index e76e877..c6ca257 100644 --- a/src/core/validators/messages.ts +++ b/src/core/validators/messages.ts @@ -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'. diff --git a/src/infrastructure/ui/index.ts b/src/infrastructure/ui/index.ts new file mode 100644 index 0000000..8dd31f7 --- /dev/null +++ b/src/infrastructure/ui/index.ts @@ -0,0 +1,5 @@ +export * from './input-time-entry-description' +export * from './input-token' +export * from './select-project' +export * from './select-workspace' + diff --git a/src/infrastructure/ui/input-time-entry-description.ts b/src/infrastructure/ui/input-time-entry-description.ts new file mode 100644 index 0000000..1d3dd3c --- /dev/null +++ b/src/infrastructure/ui/input-time-entry-description.ts @@ -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 + }); +} diff --git a/src/infrastructure/ui/input-token.ts b/src/infrastructure/ui/input-token.ts new file mode 100644 index 0000000..9df6394 --- /dev/null +++ b/src/infrastructure/ui/input-token.ts @@ -0,0 +1,7 @@ +import {input} from "@inquirer/prompts"; + +import {messages} from "../../core"; + +export function inputToken() { + return input({message: messages.forms.token}) +} diff --git a/src/infrastructure/ui/select-project.ts b/src/infrastructure/ui/select-project.ts new file mode 100644 index 0000000..e612f7c --- /dev/null +++ b/src/infrastructure/ui/select-project.ts @@ -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 + } +} diff --git a/src/infrastructure/ui/select-workspace.ts b/src/infrastructure/ui/select-workspace.ts new file mode 100644 index 0000000..ece5546 --- /dev/null +++ b/src/infrastructure/ui/select-workspace.ts @@ -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 + } +}