Skip to content

Commit 1625ce5

Browse files
author
Schmidely Stéphane
authored
Merge pull request #8 from stephlm2dev/feature/split-into-small-components
Refactor - split into small components
2 parents 59a6796 + b43fb57 commit 1625ce5

13 files changed

Lines changed: 660 additions & 60 deletions

File tree

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@
1111
"@oclif/command": "1",
1212
"@oclif/config": "1",
1313
"@oclif/plugin-autocomplete": "^0.1.0",
14-
"@oclif/plugin-help": "1"
14+
"@oclif/plugin-help": "1",
15+
"@types/inquirer": "^6.0.3",
16+
"@types/shelljs": "^0.8.5",
17+
"inquirer": "^6.3.1",
18+
"moment": "^2.24.0",
19+
"shelljs": "^0.8.3"
1520
},
1621
"devDependencies": {
1722
"@oclif/dev-cli": "1",

src/commands/export.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { Command } from '@oclif/command'
2+
import { EOL } from 'os'
3+
4+
// Our files - FIXME path import
5+
import { FLAGS, FORMATS, PROJECTS } from '../utils/commands/export/constants'
6+
import ExportUi from '../utils/commands/export/ui'
7+
import ExportUtils from '../utils/commands/export/utils'
8+
import ExportValidator from '../utils/commands/export/validators'
9+
10+
export default class Export extends Command {
11+
static description = 'export data for a specific date / project'
12+
13+
static examples = [
14+
'$ twe export --interactive',
15+
'$ twe export --format=json --interactive',
16+
'$ twe export --format=csv --project=name --from=2019-12-01 --to=2019-12-15'
17+
]
18+
19+
// https://oclif.io/docs/flags
20+
static flags = FLAGS
21+
22+
// https://oclif.io/docs/args
23+
static args = []
24+
25+
/**
26+
* Entrypoint of the `twe export` command
27+
*/
28+
async run() {
29+
const tools = {
30+
utils: new ExportUtils(),
31+
validator: new ExportValidator()
32+
}
33+
34+
// Step 1 - check requirements
35+
this.checkRequirements(tools)
36+
37+
// Step 2 - Prepare parsing
38+
let {
39+
flags, availableFlags, missingFlags, availableFlagsValues
40+
} = this.prepareCommandParsing(tools)
41+
42+
// Step 3 - check flags values (or exit)
43+
if (missingFlags.length !== 0) {
44+
const missingFlagsData = await this.askMissingFlags(
45+
flags.interactive, missingFlags, availableFlagsValues
46+
)
47+
flags = { ...flags, ...missingFlagsData }
48+
}
49+
50+
// Step 3 - Verify params
51+
const { interactive, ...params } = flags
52+
this.checkParams(tools, params, availableFlagsValues)
53+
54+
// (FIXME) Step 4 - Extract data
55+
// (FIXME) Step 5 - Filter data
56+
// (FIXME) Step 6 - Aggregate data
57+
// (FIXME) Step 7 - Save data
58+
}
59+
60+
/**
61+
* Check system requirements
62+
*/
63+
private checkRequirements(tools: any) {
64+
// Step 1 - check requirements
65+
const invalidRequirements = tools.validator.checkRequirements()
66+
if (invalidRequirements.length !== 0) {
67+
this.error(
68+
tools.utils.errorMessage('missing requirement', invalidRequirements),
69+
{ exit: 2 }
70+
)
71+
}
72+
return true
73+
}
74+
75+
/**
76+
* Prepare some stuff
77+
*/
78+
private prepareCommandParsing(tools: any) {
79+
// Prepare information
80+
const { flags } = this.parse(Export)
81+
const availableFlags = tools.utils.availableFlags(FLAGS)
82+
const missingFlags = availableFlags.filter(el => !(el in flags))
83+
const availableFlagsValues = { formats: FORMATS, projects: PROJECTS }
84+
return { flags, availableFlags, missingFlags, availableFlagsValues }
85+
}
86+
87+
/**
88+
* Ask for missing flags when interactive mode
89+
*/
90+
private async askMissingFlags(
91+
interactive: boolean, missingFlags: Array<string>, availableFlagsValues: any
92+
) {
93+
if (!interactive) {
94+
this.error('Missing parameters', { exit: 3 })
95+
}
96+
const missingFlagsData = await new ExportUi().askMissingFlags(
97+
missingFlags, availableFlagsValues
98+
)
99+
return missingFlagsData
100+
}
101+
102+
/**
103+
* Check validity of every params
104+
*/
105+
private checkParams(tools: any, params: any, availableFlagsValues: any) {
106+
const invalidParams = tools.validator.checkParams(params, availableFlagsValues)
107+
if (invalidParams.length !== 0) {
108+
this.error(
109+
tools.utils.errorMessage('invalid param', invalidParams),
110+
{ exit: 2 }
111+
)
112+
}
113+
return true
114+
}
115+
}

src/commands/hello.ts

Lines changed: 0 additions & 53 deletions
This file was deleted.
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { flags } from '@oclif/command'
2+
import { EOL } from 'os'
3+
import { exec } from 'shelljs'
4+
5+
class Constants {
6+
private _flags: any
7+
private _formats: Array<string>
8+
private _projects: Array<string>
9+
10+
constructor() {
11+
this._flags = this.setFlags()
12+
this._formats = this.setFormats()
13+
this._projects = this.setProjects()
14+
}
15+
16+
get flags() {
17+
return this._flags
18+
}
19+
20+
get formats() {
21+
return this._formats
22+
}
23+
24+
get projects() {
25+
return this._projects
26+
}
27+
28+
private setFlags() {
29+
return {
30+
// -h, --help
31+
help: flags.help({
32+
char: 'h',
33+
description: 'display help',
34+
required: false
35+
}),
36+
format: flags.string({
37+
char: 'f',
38+
description: 'output format (ie "json" or "csv")',
39+
multiple: false,
40+
required: false
41+
}),
42+
project: flags.string({
43+
char: 'p',
44+
description: 'name of the project',
45+
multiple: false,
46+
required: false
47+
}),
48+
from: flags.string({
49+
char: 'f',
50+
description: 'start date with format "YYYY-MM-DD"',
51+
multiple: false,
52+
required: false
53+
}),
54+
to: flags.string({
55+
char: 't',
56+
description: 'end date with format "YYYY-MM-DD"',
57+
multiple: false,
58+
required: false
59+
}),
60+
interactive: flags.boolean({
61+
char: 'i',
62+
description: 'interactive mode',
63+
default: false,
64+
required: false
65+
})
66+
}
67+
}
68+
69+
/**
70+
* List all available exports format
71+
*/
72+
private setFormats() {
73+
return ['csv', 'json']
74+
}
75+
76+
/**
77+
* List all projects from Taskwarrior
78+
*/
79+
private setProjects() {
80+
const command = 'task rc.list.all.projects=1 _projects'
81+
const { stdout, stderr, code } = exec(command, { silent: true })
82+
/* FIXME handle error case
83+
if (code !== 0) {
84+
this.error(stderr.trim(), { exit: code })
85+
}
86+
*/
87+
return stdout.trim().split(EOL)
88+
}
89+
}
90+
91+
const constants = new Constants()
92+
const FLAGS = constants.flags
93+
const FORMATS = constants.formats
94+
const PROJECTS = constants.projects
95+
96+
export {
97+
FLAGS,
98+
FORMATS,
99+
PROJECTS
100+
}

src/utils/commands/export/ui.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { prompt, Question } from 'inquirer'
2+
import * as moment from 'moment'
3+
4+
import { DateValidator } from './validators'
5+
6+
export default class ExportUi {
7+
constructor() {}
8+
9+
/**
10+
* Ask user for missing flags
11+
*/
12+
public async askMissingFlags(
13+
missingFlags: Array<string>, availableFlagsValues: any
14+
) {
15+
const questions = missingFlags.map(el => {
16+
let input = null
17+
switch (el) {
18+
case 'format':
19+
input = this.askFormat(availableFlagsValues.formats)
20+
break
21+
case 'project':
22+
input = this.askProject(availableFlagsValues.projects)
23+
break
24+
case 'from':
25+
input = this.askStartDate()
26+
break
27+
case 'to':
28+
input = this.askEndDate()
29+
break
30+
default:
31+
// FIXME handle error case
32+
// this.error(`You should not be here with ${el}`, { exit: 3 })
33+
}
34+
return input
35+
})
36+
37+
const answers = await this.askUser(questions)
38+
return answers
39+
}
40+
41+
/**
42+
* InquirerJS question for output format
43+
* 'json' or 'csv' are currently supported
44+
*/
45+
private askFormat(availableFormats: Array<string>): Question {
46+
const choices = availableFormats.map(format => {
47+
return { name: format.toUpperCase(), value: format }
48+
})
49+
return {
50+
type: 'list',
51+
name: 'format',
52+
message: 'Which output format do you want ?',
53+
default: choices[0].value,
54+
choices
55+
}
56+
}
57+
58+
/**
59+
* InquirerJS question for project
60+
*/
61+
private askProject(availableProjects: Array<string>): Question {
62+
return {
63+
type: 'list',
64+
name: 'project',
65+
message: 'Which project ?',
66+
default: availableProjects[0],
67+
choices: availableProjects,
68+
pageSize: availableProjects.length
69+
}
70+
}
71+
72+
/**
73+
* InquirerJS question for start date export
74+
* valide ! (use of moment.js)
75+
*/
76+
private askStartDate(): Question {
77+
return {
78+
type: 'input',
79+
name: 'from',
80+
message: 'From which date (YYYY-MM-DD) ?',
81+
default: moment().startOf('month').format('YYYY-MM-DD'),
82+
validate: new DateValidator().isValid
83+
}
84+
}
85+
86+
/**
87+
* InquirerJS question for end date export
88+
*/
89+
private askEndDate(): Question {
90+
return {
91+
type: 'input',
92+
name: 'to',
93+
message: 'Until which date (YYYY-MM-DD) ?',
94+
default: moment().endOf('month').format('YYYY-MM-DD'),
95+
validate: new DateValidator().isValid
96+
}
97+
}
98+
99+
/**
100+
* Ask user input with Inquirer.js
101+
*
102+
* @see https://github.com/SBoudrias/Inquirer.js
103+
*/
104+
private async askUser(questions: Array<Question>) {
105+
return prompt(questions)
106+
}
107+
}

0 commit comments

Comments
 (0)