Skip to content

Commit ba2ef88

Browse files
AndreiAndrei
authored andcommitted
new: optionals support
1 parent fd01945 commit ba2ef88

File tree

5 files changed

+102
-61
lines changed

5 files changed

+102
-61
lines changed

src/index.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,12 @@
1-
import path from "path";
21
import fs from "fs-extra"
32
import { Command } from "commander";
43
import logger from "./utils/logger";
54
import Invoice from "./utils/invoice";
65
import minimist from "minimist"
76
import { getConfig, getInvoiceDirectory, getVersion, openConfigDirectory, openConfigurationFile, openInvoicesDirectory, timeout } from "./utils";
8-
import * as os from "os"
97
import inquirer from "inquirer";
108
import type { Config } from "./types/types";
11-
12-
export const DEFAULT_DIR = path.join(os.homedir(), ".config", "icli/");
13-
export const DEFAULT_INVOICES_DIR = path.join(os.homedir(), "Documents", "Invoices/")
14-
export const DEFAULT_PATH = path.join(os.homedir(), ".config", "icli/icli.json");
15-
export const INVOICE_PATH = path.join(path.dirname(DEFAULT_PATH), "invoices.json");
16-
9+
import { DEFAULT_OPTION_PROMPTS, DEFAULT_PATH } from "./utils/constants";
1710

1811
async function init() {
1912

@@ -47,7 +40,8 @@ async function init() {
4740
await timeout(200)
4841

4942
let unset = []
50-
for (const defaultOption of Object.keys(Invoice.defaultOptionPrompts)) {
43+
const requiredValues = Object.entries(DEFAULT_OPTION_PROMPTS).filter(([_, v]) => v.required).map(([k]) => k)
44+
for (const defaultOption of requiredValues) {
5145
if (config?.default_values?.[defaultOption as keyof typeof config.default_values]) continue
5246

5347
unset.push(defaultOption.toLowerCase())

src/types/types.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export interface Config {
77
}
88

99
export interface DefaultOption<T extends 'input' | 'number' = 'input'> {
10+
required: boolean;
1011
type: T
1112
message: string
1213
validate: (value: T extends 'number' ? number : string) => void
@@ -32,8 +33,8 @@ export interface DefaultOptionPrompts {
3233
fullName: DefaultOption
3334
address: DefaultOption
3435
postcode: DefaultOption
36+
email: DefaultOption
3537
logo?: DefaultOption
36-
email?: DefaultOption
3738
}
3839

3940
export interface InvoiceItem {

src/utils/constants.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import path from "path"
2+
import os from "os"
3+
import type { DefaultOptionPrompts } from "../types/types";
4+
import axios from "axios";
5+
6+
export const DEFAULT_DIR = path.join(os.homedir(), ".config", "icli/");
7+
export const DEFAULT_INVOICES_DIR = path.join(os.homedir(), "Documents", "Invoices/")
8+
export const DEFAULT_PATH = path.join(os.homedir(), ".config", "icli/icli.json");
9+
export const INVOICE_PATH = path.join(path.dirname(DEFAULT_PATH), "invoices.json");
10+
11+
export const DEFAULT_OPTION_PROMPTS: DefaultOptionPrompts = {
12+
sortCode: {
13+
required: true,
14+
type: "input",
15+
message: "Enter your sort code",
16+
validate: val => {
17+
const sortCodePattern = /^\d{2}-\d{2}-\d{2}$/;
18+
return sortCodePattern.test(val) || "Sort code must be in the format XX-XX-XX (e.g., 12-34-56)";
19+
}
20+
},
21+
bankNum: {
22+
required: true,
23+
type: "number",
24+
message: "Enter your bank account number",
25+
validate: num => {
26+
const bankNumStr = num.toString();
27+
return /^\d{8}$/.test(bankNumStr) || "Bank account number must be exactly 8 digits long";
28+
}
29+
},
30+
fullName: {
31+
required: true,
32+
type: "input",
33+
message: "Enter your full name",
34+
validate: str => {
35+
const fullNamePattern = /^[a-zA-Z]+([ '-][a-zA-Z]+)+$/;
36+
return fullNamePattern.test(str) || "Full name must contain at least two parts (e.g., John Doe) and only letters, spaces, hyphens, or apostrophes";
37+
}
38+
},
39+
address: {
40+
required: true,
41+
type: "input",
42+
message: "Enter your line of address",
43+
validate: str => {
44+
const addressPattern = /^[a-zA-Z0-9\s,.\-]+$/;
45+
return (str.length >= 5 && addressPattern.test(str)) || "Address must be at least 5 characters long and can contain letters, numbers, spaces, commas, and periods.";
46+
}
47+
},
48+
email: {
49+
required: true,
50+
type: "input",
51+
message: "Enter your email address",
52+
validate: str => {
53+
if (!str) return "Email cannot be empty"
54+
55+
const email = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
56+
if (!email.test(str)) return "Please insert a valid email address."
57+
58+
return true
59+
}
60+
},
61+
postcode: {
62+
required: true,
63+
type: "input",
64+
message: "Enter your postcode",
65+
validate: str => {
66+
const postcodePattern = /^([A-Z]{1,2}\d{1,2}|[A-Z]{1,2}\d{1,2}[A-Z]?)\s?\d[A-Z]{2}$/i;
67+
return postcodePattern.test(str) || "Postcode must be in a valid format (e.g., AB1 2CD).";
68+
}
69+
},
70+
logo: {
71+
required: false,
72+
type: "input",
73+
message: "Enter your logo source",
74+
validate: async str => {
75+
if (!str) return 'Logo source cannot be empty'
76+
77+
const url = /^(ftp|http|https):\/\/[^ "]+$/;
78+
if (!url.test(str)) return "Please enter a valid URL"
79+
80+
try {
81+
const res = await axios.head(str)
82+
return res.status === 200 ? true : "The URL must point to a valid image"
83+
} catch (e) {
84+
return "Unable to access URL."
85+
}
86+
}
87+
}
88+
89+
};
90+

src/utils/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ import { exec } from "child_process"
33
import * as os from "os"
44
import path from "path"
55
import logger from "./logger"
6-
import { DEFAULT_INVOICES_DIR, DEFAULT_PATH, mainMenu } from ".."
76
import inquirer from "inquirer"
87
import fs from "fs-extra"
98
import type { Config } from "../types/types"
109
import axios from "axios"
10+
import { DEFAULT_INVOICES_DIR, DEFAULT_PATH } from "./constants"
1111

1212
export const timeout = async (t: number) => new Promise(r => setTimeout(r, t))
1313

@@ -173,7 +173,7 @@ export function getConfig(): Config {
173173

174174
export async function getVersion(): Promise<string> {
175175
try {
176-
const res = await axios.get(`https://api/.github.com/repos/itzcodex24/cli/contents/package.json`)
176+
const res = await axios.get(`https://api.github.com/repos/itzcodex24/cli/contents/package.json`)
177177
if (res.data && res.data.content) {
178178
const jsonPackage = JSON.parse(Buffer.from(res.data.content, 'base64').toString('utf-8'))
179179

src/utils/invoice.ts

Lines changed: 5 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,15 @@
11
import inquirer from "inquirer";
2-
import { INVOICE_PATH, DEFAULT_PATH, DEFAULT_DIR, mainMenu } from "..";
32
import logger from "./logger";
43
import fs from "fs-extra";
54
import path from "path"
65
import HTML from "./html";
76
import chalk from "chalk";
87
import type { DefaultOptionPrompts, TInvoice } from "../types/types";
98
import { getConfig } from ".";
9+
import { DEFAULT_DIR, DEFAULT_OPTION_PROMPTS, DEFAULT_PATH, INVOICE_PATH } from "./constants";
10+
import { mainMenu } from "..";
1011

1112
class Invoice {
12-
13-
defaultOptionPrompts: DefaultOptionPrompts = {
14-
sortCode: {
15-
type: "input",
16-
message: "Enter your sort code",
17-
validate: val => {
18-
const sortCodePattern = /^\d{2}-\d{2}-\d{2}$/;
19-
return sortCodePattern.test(val) || "Sort code must be in the format XX-XX-XX (e.g., 12-34-56)";
20-
}
21-
},
22-
bankNum: {
23-
type: "number",
24-
message: "Enter your bank account number",
25-
validate: num => {
26-
const bankNumStr = num.toString();
27-
return /^\d{8}$/.test(bankNumStr) || "Bank account number must be exactly 8 digits long";
28-
}
29-
},
30-
fullName: {
31-
type: "input",
32-
message: "Enter your full name",
33-
validate: str => {
34-
const fullNamePattern = /^[a-zA-Z]+([ '-][a-zA-Z]+)+$/;
35-
return fullNamePattern.test(str) || "Full name must contain at least two parts (e.g., John Doe) and only letters, spaces, hyphens, or apostrophes";
36-
}
37-
},
38-
address: {
39-
type: "input",
40-
message: "Enter your line of address",
41-
validate: str => {
42-
const addressPattern = /^[a-zA-Z0-9\s,.\-]+$/;
43-
return (str.length >= 5 && addressPattern.test(str)) || "Address must be at least 5 characters long and can contain letters, numbers, spaces, commas, and periods.";
44-
}
45-
},
46-
postcode: {
47-
type: "input",
48-
message: "Enter your postcode",
49-
validate: str => {
50-
const postcodePattern = /^([A-Z]{1,2}\d{1,2}|[A-Z]{1,2}\d{1,2}[A-Z]?)\s?\d[A-Z]{2}$/i;
51-
return postcodePattern.test(str) || "Postcode must be in a valid format (e.g., AB1 2CD).";
52-
}
53-
}
54-
55-
};
56-
5713
async createInvoice(useDefaultValues = true) {
5814
try {
5915
const templatePath = path.dirname(DEFAULT_PATH);
@@ -167,7 +123,7 @@ class Invoice {
167123
const value = await inquirer.prompt([
168124
{
169125
name: key,
170-
...this.defaultOptionPrompts[key]
126+
...DEFAULT_OPTION_PROMPTS[key]
171127
}
172128
])
173129
return value[key]
@@ -176,10 +132,10 @@ class Invoice {
176132
async setupDefaultValues(args?: string[]) {
177133
const config = getConfig()
178134

179-
args = args?.length ? args : Object.keys(this.defaultOptionPrompts).map(o => o.toLowerCase())
135+
args = args?.length ? args : Object.entries(DEFAULT_OPTION_PROMPTS).filter(([_, v]) => v.required).map(([k]) => k)
180136

181137
for (const arg of args) {
182-
const key = Object.keys(this.defaultOptionPrompts).find(o => o.toLowerCase() === arg)
138+
const key = Object.keys(DEFAULT_OPTION_PROMPTS).find(o => o.toLowerCase() === arg)
183139
if (!key) {
184140
logger.error(`Can't edit a value that doesn't exist: ${arg}`)
185141
continue

0 commit comments

Comments
 (0)