quasar-ui-danx 0.4.90 → 0.4.92
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/danx.es.js +2440 -2337
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +65 -65
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/Utility/Buttons/ActionButton.vue +11 -2
- package/src/helpers/actions.ts +15 -12
- package/src/helpers/formats/datetime.ts +285 -0
- package/src/helpers/formats/index.ts +4 -0
- package/src/helpers/formats/numbers.ts +127 -0
- package/src/helpers/formats/parsers.ts +74 -0
- package/src/helpers/formats/strings.ts +65 -0
- package/src/helpers/formats.ts +1 -489
package/package.json
CHANGED
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
FaSolidCopy as CopyIcon,
|
|
68
68
|
FaSolidDatabase as DatabaseIcon,
|
|
69
69
|
FaSolidEye as ViewIcon,
|
|
70
|
+
FaSolidFile as DocumentIcon,
|
|
70
71
|
FaSolidFileExport as ExportIcon,
|
|
71
72
|
FaSolidFileImport as ImportIcon,
|
|
72
73
|
FaSolidFloppyDisk as SaveIcon,
|
|
@@ -83,8 +84,8 @@ import { computed, ref } from "vue";
|
|
|
83
84
|
import { ActionTarget, ResourceAction } from "../../../types";
|
|
84
85
|
|
|
85
86
|
export interface ActionButtonProps {
|
|
86
|
-
type?: "save" | "trash" | "back" | "create" | "edit" | "copy" | "folder" | "play" | "stop" | "pause" | "refresh" | "restart" | "confirm" | "cancel" | "export" | "import" | "minus" | "merge" | "check" | "clock" | "view" | "database";
|
|
87
|
-
color?: "red" | "blue" | "blue-invert" | "sky" | "sky-invert" | "green" | "green-invert" | "lime" | "white" | "gray" | "slate" | "slate-invert" | "yellow" | "orange";
|
|
87
|
+
type?: "save" | "trash" | "back" | "create" | "edit" | "copy" | "folder" | "document" | "play" | "stop" | "pause" | "refresh" | "restart" | "confirm" | "cancel" | "export" | "import" | "minus" | "merge" | "check" | "clock" | "view" | "database";
|
|
88
|
+
color?: "red" | "blue" | "blue-invert" | "sky" | "sky-invert" | "green" | "green-invert" | "lime" | "white" | "gray" | "slate" | "slate-invert" | "yellow" | "orange" | "purple" | "teal" | "teal-invert";
|
|
88
89
|
size?: "xxs" | "xs" | "sm" | "md" | "lg";
|
|
89
90
|
icon?: object | string;
|
|
90
91
|
iconClass?: string;
|
|
@@ -164,6 +165,10 @@ const colorClass = computed(() => {
|
|
|
164
165
|
return "text-blue-900 bg-blue-300 hover:bg-blue-400";
|
|
165
166
|
case "blue-invert":
|
|
166
167
|
return "text-blue-300 bg-blue-900 hover:bg-blue-800";
|
|
168
|
+
case "teal":
|
|
169
|
+
return "text-teal-800 bg-teal-200 hover:bg-teal-400";
|
|
170
|
+
case "teal-invert":
|
|
171
|
+
return "text-teal-200 bg-teal-500 hover:bg-teal-600";
|
|
167
172
|
case "sky":
|
|
168
173
|
return "text-sky-900 bg-sky-300 hover:bg-sky-400";
|
|
169
174
|
case "sky-invert":
|
|
@@ -180,6 +185,8 @@ const colorClass = computed(() => {
|
|
|
180
185
|
return "text-slate-900 bg-slate-300 hover:bg-slate-400";
|
|
181
186
|
case "slate-invert":
|
|
182
187
|
return "text-slate-300 bg-slate-900 hover:bg-slate-800";
|
|
188
|
+
case "purple":
|
|
189
|
+
return "text-purple-300 bg-purple-900 hover:bg-purple-800";
|
|
183
190
|
default:
|
|
184
191
|
return "";
|
|
185
192
|
}
|
|
@@ -211,6 +218,8 @@ const typeOptions = computed(() => {
|
|
|
211
218
|
return { icon: CopyIcon };
|
|
212
219
|
case "folder":
|
|
213
220
|
return { icon: FolderIcon };
|
|
221
|
+
case "document":
|
|
222
|
+
return { icon: DocumentIcon };
|
|
214
223
|
case "clock":
|
|
215
224
|
return { icon: ClockIcon };
|
|
216
225
|
case "play":
|
package/src/helpers/actions.ts
CHANGED
|
@@ -25,11 +25,12 @@ export function useActions(actions: ActionOptions[], globalOptions: ActionGlobal
|
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Extend an action so the base action can be modified without affecting other places the action is used.
|
|
28
|
-
* This isolates the action to the provided id, so it is still re-usable across the system, but does not affect
|
|
28
|
+
* This isolates the action to the provided id, so it is still re-usable across the system, but does not affect
|
|
29
|
+
* behavior elsewhere.
|
|
29
30
|
*
|
|
30
|
-
* For example, when you have a list of items and you want to perform a callback on the action on a single item,
|
|
31
|
-
* with the id of the item you want to perform the action on, allowing you to perform
|
|
32
|
-
* in the list receiving the same callback.
|
|
31
|
+
* For example, when you have a list of items and you want to perform a callback on the action on a single item,
|
|
32
|
+
* you can extend the action with the id of the item you want to perform the action on, allowing you to perform
|
|
33
|
+
* behavior on a single item, instead of all instances in the list receiving the same callback.
|
|
33
34
|
*/
|
|
34
35
|
function extendAction(actionName: string, extendedId: string | number, actionOptions: Partial<ActionOptions>): ResourceAction {
|
|
35
36
|
const action = getAction(actionName);
|
|
@@ -134,7 +135,8 @@ export function useActions(actions: ActionOptions[], globalOptions: ActionGlobal
|
|
|
134
135
|
confirm: async (confirmInput: any) => {
|
|
135
136
|
|
|
136
137
|
// Resolve the input based on the useInputFromConfirm option
|
|
137
|
-
// Not setting useInputFromConfirm will merge the input from the confirm dialog with the input
|
|
138
|
+
// Not setting useInputFromConfirm will merge the input from the confirm dialog with the input
|
|
139
|
+
// from the action
|
|
138
140
|
let resolvedInput;
|
|
139
141
|
if (action.useInputFromConfirm === false) {
|
|
140
142
|
resolvedInput = input;
|
|
@@ -207,7 +209,8 @@ function setTargetSavingState(target: ActionTarget, saving: boolean) {
|
|
|
207
209
|
/**
|
|
208
210
|
* Execute the confirmed action on the target (ie: calling the server, or whatever the callback function does).
|
|
209
211
|
*
|
|
210
|
-
* 1. If the action has an optimistic callback, it will be called before the actual action to immediately update the UI
|
|
212
|
+
* 1. If the action has an optimistic callback, it will be called before the actual action to immediately update the UI
|
|
213
|
+
* (non batch actions only).
|
|
211
214
|
* 2. Call the onBatchAction or onAction callback of the action object, depending on if the target is an array or not.
|
|
212
215
|
* 3. Call the onSuccess or onError callback based on the result of the action.
|
|
213
216
|
* 4. Call the onFinish callback of the action object.
|
|
@@ -310,11 +313,11 @@ async function onConfirmAction(action: ActionOptions, target: ActionTarget, inpu
|
|
|
310
313
|
export function withDefaultActions(label: string, listController?: ListController): ActionOptions[] {
|
|
311
314
|
return [
|
|
312
315
|
{
|
|
313
|
-
name: "
|
|
314
|
-
alias: "create"
|
|
316
|
+
name: "create"
|
|
315
317
|
},
|
|
316
318
|
{
|
|
317
|
-
name: "create",
|
|
319
|
+
name: "create-with-name",
|
|
320
|
+
alias: "create",
|
|
318
321
|
label: "Create " + label,
|
|
319
322
|
vnode: () => h(CreateNewWithNameDialog, { title: "Create " + label }),
|
|
320
323
|
onFinish: listController && ((result) => {
|
|
@@ -345,11 +348,11 @@ export function withDefaultActions(label: string, listController?: ListControlle
|
|
|
345
348
|
onAction: (action, target) => listController?.activatePanel(target, "edit")
|
|
346
349
|
},
|
|
347
350
|
{
|
|
348
|
-
name: "
|
|
349
|
-
alias: "delete"
|
|
351
|
+
name: "delete"
|
|
350
352
|
},
|
|
351
353
|
{
|
|
352
|
-
name: "delete",
|
|
354
|
+
name: "delete-with-confirm",
|
|
355
|
+
alias: "delete",
|
|
353
356
|
label: "Delete",
|
|
354
357
|
class: "text-red-500",
|
|
355
358
|
iconClass: "text-red-500",
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { DateTime, IANAZone } from "luxon";
|
|
2
|
+
import { fDateOptions } from "../../types";
|
|
3
|
+
|
|
4
|
+
const SERVER_TZ = new IANAZone("America/Chicago");
|
|
5
|
+
|
|
6
|
+
export { DateTime, SERVER_TZ };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Converts a date string from the server's time zone to the user's time zone.
|
|
10
|
+
* @param {String} dateTimeString
|
|
11
|
+
* @returns {DateTime}
|
|
12
|
+
*/
|
|
13
|
+
export function localizedDateTime(dateTimeString: string) {
|
|
14
|
+
dateTimeString = dateTimeString?.replace("T", " ");
|
|
15
|
+
// noinspection JSCheckFunctionSignatures
|
|
16
|
+
return DateTime.fromSQL(dateTimeString, { zone: SERVER_TZ }).setZone("local");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Converts a date string from the user's time zone to the server's time zone.
|
|
21
|
+
* @param dateTimeString
|
|
22
|
+
* @returns {DateTime}
|
|
23
|
+
*/
|
|
24
|
+
export function remoteDateTime(dateTimeString: string) {
|
|
25
|
+
dateTimeString = dateTimeString?.replace("T", " ");
|
|
26
|
+
// noinspection JSCheckFunctionSignatures
|
|
27
|
+
return DateTime.fromSQL(dateTimeString, { zone: "local" }).setZone(SERVER_TZ);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Formats a Luxon DateTime object into a Quasar formatted date string
|
|
32
|
+
* @param date
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
export function fQDate(date: string) {
|
|
36
|
+
return fDate(date, { format: "yyyy/MM/dd" });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
*
|
|
41
|
+
* @param {String} dateTimeString
|
|
42
|
+
* @param options
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
export function fLocalizedDateTime(dateTimeString: string, options = {}) {
|
|
46
|
+
return fDateTime(localizedDateTime(dateTimeString), options);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Formats a date/time object or string into a human-readable format
|
|
51
|
+
*
|
|
52
|
+
* @param {String|Object} dateTime
|
|
53
|
+
* @param format
|
|
54
|
+
* @param {String|null} empty
|
|
55
|
+
* @returns {string}
|
|
56
|
+
*/
|
|
57
|
+
export function fDateTime(
|
|
58
|
+
dateTime: string | DateTime | null = null,
|
|
59
|
+
{ format = "M/d/yy h:mma", empty = "- -" }: fDateOptions = {}
|
|
60
|
+
) {
|
|
61
|
+
const formatted = parseDateTime(dateTime)?.toFormat(format).toLowerCase();
|
|
62
|
+
return formatted || empty;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function fDateTimeMs(
|
|
66
|
+
dateTime: string | DateTime | null = null,
|
|
67
|
+
{ empty = "- -" }: fDateOptions = {}
|
|
68
|
+
) {
|
|
69
|
+
const formatted = parseDateTime(dateTime)?.toFormat("M/d/yy H:mm:ss.SSS").toLowerCase();
|
|
70
|
+
return formatted || empty;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Formats a date/time object or string into the best format for DB input
|
|
75
|
+
* @param dateTime
|
|
76
|
+
* @returns {string}
|
|
77
|
+
*/
|
|
78
|
+
export function dbDateTime(dateTime: string | DateTime | null = null) {
|
|
79
|
+
return fDateTime(dateTime, { format: "yyyy-MM-dd HH:mm:ss", empty: undefined });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Formats a date object or string into a human-readable format
|
|
84
|
+
* @param {String|Object} dateTime
|
|
85
|
+
* @param {String|null} empty
|
|
86
|
+
* @param format
|
|
87
|
+
* @returns {string}
|
|
88
|
+
*/
|
|
89
|
+
export function fDate(dateTime: string | DateTime | null, { empty = "--", format = "M/d/yy" }: fDateOptions = {}) {
|
|
90
|
+
const formatted = parseDateTime(dateTime)?.toFormat(format || "M/d/yy");
|
|
91
|
+
return formatted || empty;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Parses a date string into a Luxon DateTime object
|
|
97
|
+
*/
|
|
98
|
+
export function parseDateTime(dateTime: string | DateTime | number | null): DateTime<boolean> | null {
|
|
99
|
+
if (typeof dateTime === "number") {
|
|
100
|
+
return DateTime.fromMillis(dateTime as number);
|
|
101
|
+
}
|
|
102
|
+
if (typeof dateTime === "string") {
|
|
103
|
+
return parseGenericDateTime(dateTime);
|
|
104
|
+
}
|
|
105
|
+
return dateTime as DateTime<boolean> || DateTime.fromSQL("0000-00-00 00:00:00");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Parses a SQL formatted date string into a Luxon DateTime object
|
|
110
|
+
*/
|
|
111
|
+
export function parseSqlDateTime(dateTime: string) {
|
|
112
|
+
const parsed = DateTime.fromSQL(dateTime.replace("T", " ").replace(/\//g, "-"));
|
|
113
|
+
return parsed.isValid ? parsed : null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Parses a Quasar formatted date string into a Luxon DateTime object
|
|
118
|
+
*/
|
|
119
|
+
export function parseQDate(date: string, format = "yyyy/MM/dd"): DateTime<boolean> | null {
|
|
120
|
+
const parsed = DateTime.fromFormat(date, format);
|
|
121
|
+
return parsed.isValid ? parsed : null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Parses a Quasar formatted date/time string into a Luxon DateTime object
|
|
126
|
+
*/
|
|
127
|
+
export function parseQDateTime(date: string, format = "yyyy/MM/dd HH:mm:ss"): DateTime<boolean> | null {
|
|
128
|
+
const parsed = DateTime.fromFormat(date, format);
|
|
129
|
+
return parsed.isValid ? parsed : null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Parses a date string in various formats into a Luxon DateTime object.
|
|
134
|
+
* Tries a list of common formats until one works.
|
|
135
|
+
*
|
|
136
|
+
* @param {string} dateTimeString - The date string to parse.
|
|
137
|
+
* @param {string} [defaultZone="local"] - The default time zone to use if not specified.
|
|
138
|
+
* @returns {DateTime | null} - A Luxon DateTime object if parsing succeeds, otherwise null.
|
|
139
|
+
*/
|
|
140
|
+
export function parseGenericDateTime(dateTimeString: string, defaultZone = "local"): DateTime | null {
|
|
141
|
+
if (!dateTimeString) return null;
|
|
142
|
+
|
|
143
|
+
const formats = [
|
|
144
|
+
"yyyy-MM-dd", // ISO date
|
|
145
|
+
"yyyy-MM-dd HH:mm:ss", // ISO date with time
|
|
146
|
+
"MM/dd/yyyy", // US-style date
|
|
147
|
+
"dd/MM/yyyy", // European-style date
|
|
148
|
+
"MM/dd/yy", // US short date
|
|
149
|
+
"dd/MM/yy", // European short date
|
|
150
|
+
"yyyy/MM/dd", // Alternative ISO
|
|
151
|
+
"MM-dd-yyyy", // US with dashes
|
|
152
|
+
"dd-MM-yyyy", // European with dashes
|
|
153
|
+
"M/d/yyyy", // US date without leading zeros
|
|
154
|
+
"d/M/yyyy", // European date without leading zeros
|
|
155
|
+
"yyyyMMdd" // Compact ISO
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
for (const format of formats) {
|
|
159
|
+
const parsed = DateTime.fromFormat(dateTimeString, format, { zone: defaultZone });
|
|
160
|
+
if (parsed.isValid) {
|
|
161
|
+
return parsed;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Fallback to ISO parsing for strings like "2022-11-18T10:10:10Z"
|
|
166
|
+
const isoParsed = DateTime.fromISO(dateTimeString, { zone: defaultZone });
|
|
167
|
+
if (isoParsed.isValid) {
|
|
168
|
+
return isoParsed;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Fallback to SQL parsing for strings like "2022-11-18 10:10:10"
|
|
172
|
+
const sqlParsed = DateTime.fromSQL(dateTimeString, { zone: defaultZone });
|
|
173
|
+
if (sqlParsed.isValid) {
|
|
174
|
+
return sqlParsed;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Formats a number of seconds into Hours / Minutes / Seconds or just Minutes and Seconds
|
|
182
|
+
*
|
|
183
|
+
* @param second
|
|
184
|
+
* @returns {string}
|
|
185
|
+
*/
|
|
186
|
+
export function fSecondsToTime(second: number) {
|
|
187
|
+
const time = DateTime.now().setZone("UTC").startOf("year").set({ second });
|
|
188
|
+
const hours = Math.floor(second / 3600);
|
|
189
|
+
return (hours ? hours + ":" : "") + time.toFormat("mm:ss");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Formats a number of seconds into a duration string in 00h 00m 00s format
|
|
194
|
+
*/
|
|
195
|
+
export function fSecondsToDuration(seconds: number) {
|
|
196
|
+
const hours = Math.floor(seconds / 3600);
|
|
197
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
198
|
+
const secs = Math.floor(seconds % 60);
|
|
199
|
+
return `${hours ? hours + "h " : ""}${minutes ? minutes + "m " : ""}${secs}s`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Formats a number of milliseconds into a duration string in 00h 00m 00s 000ms format
|
|
204
|
+
*/
|
|
205
|
+
export function fMillisecondsToDuration(milliseconds: number) {
|
|
206
|
+
const durStr = fSecondsToDuration(Math.floor(milliseconds / 1000));
|
|
207
|
+
return (durStr === "0s" ? "" : durStr) + ` ${Math.floor(milliseconds % 1000)}ms`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Formats a duration between two date strings in 00h 00m 00s format
|
|
213
|
+
*/
|
|
214
|
+
export function fDuration(start: string | number, end?: string | number) {
|
|
215
|
+
const endDateTime = end ? parseDateTime(end) : DateTime.now();
|
|
216
|
+
const diff = endDateTime?.diff(parseDateTime(start) || DateTime.now(), ["hours", "minutes", "seconds"]);
|
|
217
|
+
if (!diff?.isValid) {
|
|
218
|
+
return "-";
|
|
219
|
+
}
|
|
220
|
+
const totalSeconds = diff.as("seconds");
|
|
221
|
+
return fSecondsToDuration(totalSeconds);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Formats a date/time as a relative time string (e.g., "5 minutes ago", "yesterday")
|
|
226
|
+
* @param {String|DateTime|number} dateTime - The date/time to format
|
|
227
|
+
* @returns {string} - A human-readable relative time string
|
|
228
|
+
*/
|
|
229
|
+
export function fTimeAgo(dateTime: string | DateTime | number | null): string {
|
|
230
|
+
if (!dateTime) return "";
|
|
231
|
+
|
|
232
|
+
const date = parseDateTime(dateTime);
|
|
233
|
+
if (!date) return "";
|
|
234
|
+
|
|
235
|
+
const now = DateTime.now();
|
|
236
|
+
const diffTime = Math.abs(now.toMillis() - date.toMillis());
|
|
237
|
+
|
|
238
|
+
// Convert to different units
|
|
239
|
+
const diffSeconds = Math.floor(diffTime / 1000);
|
|
240
|
+
const diffMinutes = Math.floor(diffTime / (1000 * 60));
|
|
241
|
+
const diffHours = Math.floor(diffTime / (1000 * 60 * 60));
|
|
242
|
+
|
|
243
|
+
// Seconds
|
|
244
|
+
if (diffSeconds < 10) {
|
|
245
|
+
return "a few seconds ago";
|
|
246
|
+
} else if (diffSeconds < 60) {
|
|
247
|
+
return `${diffSeconds} seconds ago`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Minutes
|
|
251
|
+
if (diffMinutes === 1) {
|
|
252
|
+
return "a minute ago";
|
|
253
|
+
} else if (diffMinutes < 60) {
|
|
254
|
+
return `${diffMinutes} minutes ago`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Hours
|
|
258
|
+
if (diffHours === 1) {
|
|
259
|
+
return "an hour ago";
|
|
260
|
+
} else if (diffHours < 24) {
|
|
261
|
+
return `${diffHours} hours ago`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Calendar-based day logic
|
|
265
|
+
const today = now.startOf("day");
|
|
266
|
+
const authDay = date.startOf("day");
|
|
267
|
+
const daysDiff = Math.floor(today.diff(authDay, "days").days);
|
|
268
|
+
|
|
269
|
+
if (daysDiff === 0) {
|
|
270
|
+
return "today";
|
|
271
|
+
} else if (daysDiff === 1) {
|
|
272
|
+
return "yesterday";
|
|
273
|
+
} else if (daysDiff < 7) {
|
|
274
|
+
return `${daysDiff} days ago`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Weeks
|
|
278
|
+
if (daysDiff < 30) {
|
|
279
|
+
const weeks = Math.floor(daysDiff / 7);
|
|
280
|
+
return weeks === 1 ? "a week ago" : `${weeks} weeks ago`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 30+ days: show full date
|
|
284
|
+
return date.toLocaleString({ year: "numeric", month: "short", day: "numeric" });
|
|
285
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats an amount into USD currency format
|
|
3
|
+
*/
|
|
4
|
+
export function fCurrency(amount: number, options?: object) {
|
|
5
|
+
if (amount === null || amount === undefined || isNaN(amount)) {
|
|
6
|
+
return "$-";
|
|
7
|
+
}
|
|
8
|
+
return new Intl.NumberFormat("en-US", {
|
|
9
|
+
style: "currency",
|
|
10
|
+
currency: "USD",
|
|
11
|
+
...options
|
|
12
|
+
}).format(amount);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Formats an amount into USD currency format without cents
|
|
17
|
+
*/
|
|
18
|
+
export function fCurrencyNoCents(amount: number, options?: object) {
|
|
19
|
+
return fCurrency(amount, {
|
|
20
|
+
maximumFractionDigits: 0,
|
|
21
|
+
...options
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Formats a number into a human-readable format
|
|
27
|
+
*/
|
|
28
|
+
export function fNumber(number: number, options?: object) {
|
|
29
|
+
return new Intl.NumberFormat("en-US", options).format(number);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Formats a currency into a shorthand human-readable format (ie: $1.2M or $5K)
|
|
34
|
+
*/
|
|
35
|
+
export function fShortCurrency(value: string | number, options?: { round: boolean }) {
|
|
36
|
+
return "$" + fShortNumber(value, options);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Formats a number into a shorthand human-readable format (ie: 1.2M or 5K)
|
|
41
|
+
*/
|
|
42
|
+
export function fShortNumber(value: string | number, options?: { round: boolean }) {
|
|
43
|
+
if (value === "" || value === null || value === undefined) {
|
|
44
|
+
return "-";
|
|
45
|
+
}
|
|
46
|
+
const shorts = [
|
|
47
|
+
{ pow: 3, unit: "K" },
|
|
48
|
+
{ pow: 6, unit: "M" },
|
|
49
|
+
{ pow: 9, unit: "B" },
|
|
50
|
+
{ pow: 12, unit: "T" },
|
|
51
|
+
{ pow: 15, unit: "Q" }
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
let n = Math.round(+value);
|
|
55
|
+
|
|
56
|
+
const short = shorts.find(({ pow }) => Math.pow(10, pow) < n && Math.pow(10, pow + 3) > n) || null;
|
|
57
|
+
|
|
58
|
+
if (short) {
|
|
59
|
+
n = n / Math.pow(10, short.pow);
|
|
60
|
+
return options?.round
|
|
61
|
+
? n + short.unit
|
|
62
|
+
: n.toFixed(n > 100 ? 0 : 1) + short.unit;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return n;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Formats a number into a human-readable size format (ie: 1.2MB or 5KB)
|
|
70
|
+
*/
|
|
71
|
+
export function fShortSize(value: string | number) {
|
|
72
|
+
const powers = [
|
|
73
|
+
{ pow: 0, unit: "B" },
|
|
74
|
+
{ pow: 10, unit: "KB" },
|
|
75
|
+
{ pow: 20, unit: "MB" },
|
|
76
|
+
{ pow: 30, unit: "GB" },
|
|
77
|
+
{ pow: 40, unit: "TB" },
|
|
78
|
+
{ pow: 50, unit: "PB" },
|
|
79
|
+
{ pow: 60, unit: "EB" },
|
|
80
|
+
{ pow: 70, unit: "ZB" },
|
|
81
|
+
{ pow: 80, unit: "YB" }
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const n = Math.round(+value);
|
|
85
|
+
const power = powers.find((p, i) => {
|
|
86
|
+
const nextPower = powers[i + 1];
|
|
87
|
+
return !nextPower || n < Math.pow(2, nextPower.pow + 10);
|
|
88
|
+
}) || powers[powers.length - 1];
|
|
89
|
+
|
|
90
|
+
const div = Math.pow(2, power.pow);
|
|
91
|
+
|
|
92
|
+
return Math.round(n / div) + " " + power.unit;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function fBoolean(value?: boolean | string | any) {
|
|
96
|
+
switch (value) {
|
|
97
|
+
case "Yes":
|
|
98
|
+
case "No":
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return (value === undefined || value === null) ? "-" : (value ? "Yes" : "No");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface FPercentOptions {
|
|
106
|
+
multiplier?: number,
|
|
107
|
+
maximumFractionDigits?: number,
|
|
108
|
+
NaN?: string
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Formats a number into a percentage
|
|
113
|
+
* @param num
|
|
114
|
+
* @param options
|
|
115
|
+
* @returns {string}
|
|
116
|
+
*/
|
|
117
|
+
export function fPercent(num: string | number, options: FPercentOptions = {}) {
|
|
118
|
+
options = { multiplier: 100, maximumFractionDigits: 1, NaN: "N/A", ...options };
|
|
119
|
+
|
|
120
|
+
num = parseFloat("" + num);
|
|
121
|
+
|
|
122
|
+
if (isNaN(num)) {
|
|
123
|
+
return options.NaN;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return fNumber(num * (options.multiplier || 100), options) + "%";
|
|
127
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { parse as parseYAML, stringify as stringifyYAML } from "yaml";
|
|
2
|
+
import { isJSON } from "../utils";
|
|
3
|
+
|
|
4
|
+
export function fJSON(string: string | object) {
|
|
5
|
+
if (!string) {
|
|
6
|
+
return string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
if (typeof string === "object") {
|
|
11
|
+
return JSON.stringify(string, null, 2);
|
|
12
|
+
}
|
|
13
|
+
return JSON.stringify(JSON.parse(string), null, 2);
|
|
14
|
+
} catch (e) {
|
|
15
|
+
return string;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Convert markdown formatted string into a valid JSON object
|
|
21
|
+
*/
|
|
22
|
+
export function parseMarkdownJSON(string: string | object): object | null | false {
|
|
23
|
+
if (!string) return null;
|
|
24
|
+
if (typeof string === "object") return string as object;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(parseMarkdownCode(string));
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function parseMarkdownYAML(string: string): object | null | false {
|
|
34
|
+
if (!string) return null;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
return parseYAML(parseMarkdownCode(string)) || (string ? undefined : null);
|
|
38
|
+
} catch (e) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parse a markdown formatted string and return the code block content
|
|
45
|
+
*/
|
|
46
|
+
export function parseMarkdownCode(string: string): string {
|
|
47
|
+
return string.replace(/^```[a-z0-9]{0,6}\s/, "").replace(/```$/, "");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Convert a JSON object or string of code into a markdown formatted JSON string
|
|
52
|
+
* ie: a valid JSON string with a ```json prefix and ``` postfix
|
|
53
|
+
*/
|
|
54
|
+
export function fMarkdownCode(type: string, string: string | object): string {
|
|
55
|
+
if (typeof string === "object" || isJSON(string)) {
|
|
56
|
+
switch (type) {
|
|
57
|
+
case "yaml":
|
|
58
|
+
string = stringifyYAML(typeof string === "string" ? JSON.parse(string) : string);
|
|
59
|
+
break;
|
|
60
|
+
case "ts":
|
|
61
|
+
default:
|
|
62
|
+
string = fJSON(string);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const regex = new RegExp(`\`\`\`${type}`, "g");
|
|
67
|
+
string = (string || "") as string;
|
|
68
|
+
if (!string.match(regex)) {
|
|
69
|
+
string = parseMarkdownCode(string as string);
|
|
70
|
+
return `\`\`\`${type}\n${string}\n\`\`\``;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return string as string;
|
|
74
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { ActionTargetItem } from "../../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Truncates the string by removing chars from the middle of the string
|
|
5
|
+
* @param str
|
|
6
|
+
* @param maxLength
|
|
7
|
+
* @returns {string|*}
|
|
8
|
+
*/
|
|
9
|
+
export function centerTruncate(str: string, maxLength: number) {
|
|
10
|
+
if (str.length > maxLength) {
|
|
11
|
+
const frontCharCount = Math.floor((maxLength - 3) / 2);
|
|
12
|
+
const backCharCount = maxLength - frontCharCount - 3;
|
|
13
|
+
return (
|
|
14
|
+
str.substring(0, frontCharCount) +
|
|
15
|
+
"..." +
|
|
16
|
+
str.substring(str.length - backCharCount)
|
|
17
|
+
);
|
|
18
|
+
} else {
|
|
19
|
+
return str;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function fPhone(value: string | number) {
|
|
24
|
+
if (!value || typeof value !== "string") {
|
|
25
|
+
return value || "";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const input = value.replace(/\D/g, "").split("");
|
|
29
|
+
let phone = "";
|
|
30
|
+
|
|
31
|
+
const startsWithOne = input.length > 0 && input[0] === "1";
|
|
32
|
+
const shift = startsWithOne ? 1 : 0;
|
|
33
|
+
|
|
34
|
+
input.map((number, index) => {
|
|
35
|
+
switch (index) {
|
|
36
|
+
case shift:
|
|
37
|
+
phone += "(";
|
|
38
|
+
break;
|
|
39
|
+
case shift + 3:
|
|
40
|
+
phone += ") ";
|
|
41
|
+
break;
|
|
42
|
+
case shift + 6:
|
|
43
|
+
phone += "-";
|
|
44
|
+
break;
|
|
45
|
+
case shift + 10:
|
|
46
|
+
phone += " x";
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
if (index === 0 && number === "1") {
|
|
50
|
+
phone += "+1 ";
|
|
51
|
+
} else {
|
|
52
|
+
phone += number;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (value === "+1 (") {
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return phone;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function fNameOrCount(items: ActionTargetItem[] | ActionTargetItem, label: string) {
|
|
64
|
+
return Array.isArray(items) ? `${items?.length} ${label}` : `${items ? items.title || items.name || items.id : ""}`;
|
|
65
|
+
}
|