diff --git a/frontend/app/utils/format-duration.js b/frontend/app/utils/format-duration.ts similarity index 66% rename from frontend/app/utils/format-duration.js rename to frontend/app/utils/format-duration.ts index 87f89227e..d631c75ba 100644 --- a/frontend/app/utils/format-duration.js +++ b/frontend/app/utils/format-duration.ts @@ -3,21 +3,18 @@ * @submodule timed-utils * @public */ -import moment from "moment"; +import moment, { type Duration } from "moment"; import { pad2joincolon } from "timed/utils/pad"; const { floor, abs } = Math; /** * Converts a moment duration into a string with zeropadded digits - * - * @function formatDuration - * @param {moment.duration} duration The duration to format - * @param {Boolean} seconds Whether to show seconds - * @return {String} The formatted duration - * @public */ -export default function formatDuration(duration, seconds = true) { +export default function formatDuration( + duration: Duration | number, + seconds: boolean = true +): string { if (typeof duration === "number") { duration = moment.duration(duration); } @@ -26,7 +23,7 @@ export default function formatDuration(duration, seconds = true) { return seconds ? "--:--:--" : "--:--"; } - const prefix = duration < 0 ? "-" : ""; + const prefix = +duration < 0 ? "-" : ""; const hours = floor(abs(duration.asHours())); const minutes = abs(duration.minutes()); diff --git a/frontend/app/utils/humanize-duration.js b/frontend/app/utils/humanize-duration.ts similarity index 65% rename from frontend/app/utils/humanize-duration.js rename to frontend/app/utils/humanize-duration.ts index b459f5b99..18e6a9e0d 100644 --- a/frontend/app/utils/humanize-duration.js +++ b/frontend/app/utils/humanize-duration.ts @@ -4,24 +4,23 @@ * @public */ +import type { Duration } from "moment"; + const { abs, floor } = Math; /** * Converts a moment duration into a string with hours minutes and optionally * seconds - * - * @function humanizeDuration - * @param {moment.duration} duration The duration to format - * @param {Boolean} seconds Whether to show seconds - * @return {String} The formatted duration - * @public */ -export default function humanizeDuration(duration, seconds = false) { +export default function humanizeDuration( + duration: Duration, + seconds: boolean = false +): string { if (!duration || duration.milliseconds() < 0) { return seconds ? "0h 0m 0s" : "0h 0m"; } - const prefix = duration < 0 ? "-" : ""; + const prefix = +duration < 0 ? "-" : ""; // TODO: The locale should be defined by the browser const h = floor(abs(duration.asHours())).toLocaleString("de-CH"); diff --git a/frontend/app/utils/pad.js b/frontend/app/utils/pad.ts similarity index 56% rename from frontend/app/utils/pad.js rename to frontend/app/utils/pad.ts index 4221daf1c..00026ec51 100644 --- a/frontend/app/utils/pad.js +++ b/frontend/app/utils/pad.ts @@ -6,13 +6,8 @@ /** * Pad items with 0 and join them with a colon - * - * @function pad2joincolon - * @param {any[]} items - The items to pad and join - * @return {String} The joined string - * @public */ -function pad2joincolon(...items) { +function pad2joincolon(...items: unknown[]): string { return items.map((v) => String(v).padStart(2, "0")).join(":"); } diff --git a/frontend/app/utils/parse-django-duration.js b/frontend/app/utils/parse-django-duration.js deleted file mode 100644 index 797b6a95e..000000000 --- a/frontend/app/utils/parse-django-duration.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @module timed - * @submodule timed-utils - * @public - */ -import moment from "moment"; - -/** - * Converts a django duration string to a moment duration - * - * @function parseDjangoDuration - * @param {String} str The django duration string representation - * @return {moment.duration} The parsed duration - * @public - */ -export default function parseDjangoDuration(str) { - if (!str) { - return null; - } - - const re = new RegExp(/^(-?\d+)?\s?(\d{2}):(\d{2}):(\d{2})(\.\d{6})?$/); - - const [, days, hours, minutes, seconds, microseconds] = str - .match(re) - .map((m) => Number(m) || 0); - - return moment.duration({ - days, - hours, - minutes, - seconds, - milliseconds: microseconds * 1000, - }); -} diff --git a/frontend/app/utils/parse-django-duration.ts b/frontend/app/utils/parse-django-duration.ts new file mode 100644 index 000000000..63a999be5 --- /dev/null +++ b/frontend/app/utils/parse-django-duration.ts @@ -0,0 +1,54 @@ +/** + * @module timed + * @submodule timed-utils + * @public + */ +import moment, { type Duration } from "moment"; + +interface Groups { + days?: number; + hours: number; + minutes: number; + seconds: number; + microseconds?: number; +} + +/** + * Converts a django duration string to a moment duration + */ +export default function parseDjangoDuration(str: string): Duration | null { + if (!str) { + return null; + } + + const re = + /^(?-?\d+)?\s?(?\d{2}):(?\d{2}):(?\d{2})(?\.\d{6})?$/; + + const matches = str.match(re) as { + groups: Groups; + } | null; + + if (!matches) { + return null; + } + + const { + days: _days, + hours, + minutes, + seconds, + microseconds: _microseconds, + } = matches.groups; + + const [days, microseconds] = [_days, _microseconds].map( + (v) => Number(v) || 0 + ) as [number, number]; + + return moment.duration({ + days, + hours, + minutes, + seconds, + milliseconds: microseconds * 1000, + }); +} diff --git a/frontend/app/utils/parse-filename.js b/frontend/app/utils/parse-filename.ts similarity index 93% rename from frontend/app/utils/parse-filename.js rename to frontend/app/utils/parse-filename.ts index c74220a37..def18da19 100644 --- a/frontend/app/utils/parse-filename.js +++ b/frontend/app/utils/parse-filename.ts @@ -11,7 +11,7 @@ const REGEX = /filename[^;=\n]*=(?(?['"]).*?\k|[^;\n]*)/; -const parseFileName = (contentDisposition) => { +const parseFileName = (contentDisposition: string) => { const { quote, filename } = REGEX.exec(contentDisposition)?.groups ?? {}; if (!filename) return "Unknown file"; const _filename = filename.startsWith("utf-8''") diff --git a/frontend/app/utils/query-params.js b/frontend/app/utils/query-params.ts similarity index 64% rename from frontend/app/utils/query-params.js rename to frontend/app/utils/query-params.ts index 18515e508..aa9bf9d19 100644 --- a/frontend/app/utils/query-params.js +++ b/frontend/app/utils/query-params.ts @@ -1,3 +1,4 @@ +import type Controller from "@ember/controller"; import { get } from "@ember/object"; import { underscore } from "@ember/string"; import { @@ -8,25 +9,33 @@ import { /** * Filter params by key */ -export const filterQueryParams = (params, ...keys) => { - return Object.keys(params).reduce((obj, key) => { - return keys.includes(key) ? obj : { ...obj, [key]: get(params, key) }; - }, {}); +export const filterQueryParams = < + T extends Record, + K extends string[] +>( + params: T, + ...keys: K +) => { + return Object.fromEntries( + Object.entries(params).filter(([k]) => !keys.includes(k)) + ) as Omit; }; /** * Underscore all object keys */ -export const underscoreQueryParams = (params) => { - return Object.keys(params).reduce((obj, key) => { - return { ...obj, [underscore(key)]: get(params, key) }; - }, {}); -}; +export const underscoreQueryParams = (params: Record) => + Object.fromEntries( + Object.entries(params).map(([k, v]) => [underscore(k), v]) + ); -export const serializeQueryParams = (params, queryParamsObject) => { +export const serializeQueryParams = >( + params: T, + queryParamsObject: { [K in keyof T]?: (deserialized: T[K]) => string } +) => { return Object.keys(params).reduce((parsed, key) => { - const serializeFn = get(queryParamsObject, key)?.serialize; - const value = get(params, key); + const serializeFn = queryParamsObject[key as keyof T]; + const value = params[key as keyof T]; return key === "type" ? parsed @@ -34,22 +43,19 @@ export const serializeQueryParams = (params, queryParamsObject) => { ...parsed, [key]: serializeFn ? serializeFn(value) : value, }; - }, {}); + }, {} as Record); }; /** - * - * @param {string} param - * @returns {string} | {undefined} * ? in all controllers, the only parameter that have the default value is `ordering`, and the value is "-date" */ -export function getDefaultQueryParamValue(param) { +export function getDefaultQueryParamValue(param: string) { if (param === "ordering") return "-date"; else if (param === "type") return "year"; return undefined; } -export function allQueryParams(controller) { +export function allQueryParams(controller: C) { const params = {}; for (const qpKey of controller.queryParams) { params[qpKey] = controller[qpKey]; @@ -57,7 +63,7 @@ export function allQueryParams(controller) { return params; } -export function queryParamsState(controller) { +export function queryParamsState(controller: C) { const states = {}; for (const param of controller.queryParams) { const defaultValue = getDefaultQueryParamValue(param); @@ -94,7 +100,10 @@ export function queryParamsState(controller) { return states; } -export function resetQueryParams(controller, ...args) { +export function resetQueryParams( + controller: C, + ...args: string[] +) { if (!args[0]) { return; } diff --git a/frontend/app/utils/serialize-moment.js b/frontend/app/utils/serialize-moment.ts similarity index 58% rename from frontend/app/utils/serialize-moment.js rename to frontend/app/utils/serialize-moment.ts index 256ad3f14..83246b786 100644 --- a/frontend/app/utils/serialize-moment.js +++ b/frontend/app/utils/serialize-moment.ts @@ -1,13 +1,13 @@ -import moment from "moment"; +import moment, { type Moment, type MomentInput } from "moment"; export const DATE_FORMAT = "YYYY-MM-DD"; -export function serializeMoment(momentObject) { +export function serializeMoment(momentObject: Moment) { if (momentObject) { momentObject = moment(momentObject); } return (momentObject && momentObject.format(DATE_FORMAT)) || null; } -export function deserializeMoment(momentString) { +export function deserializeMoment(momentString: MomentInput) { return (momentString && moment(momentString, DATE_FORMAT)) || null; } diff --git a/frontend/app/utils/url.js b/frontend/app/utils/url.js deleted file mode 100644 index 8af6f806a..000000000 --- a/frontend/app/utils/url.js +++ /dev/null @@ -1,11 +0,0 @@ -const notNullOrUndefined = (value) => value !== null && value !== undefined; - -export const cleanParams = (params) => - Object.keys(params) - .filter((key) => notNullOrUndefined(params[key])) - .reduce((cleaned, key) => ({ ...cleaned, [key]: params[key] }), {}); - -export const toQueryString = (params) => - Object.keys(params) - .map((key) => `${key}=${params[key]}`) - .join("&"); diff --git a/frontend/app/utils/url.ts b/frontend/app/utils/url.ts new file mode 100644 index 000000000..fec1269d4 --- /dev/null +++ b/frontend/app/utils/url.ts @@ -0,0 +1,25 @@ +type ExcludeNullOrUndefined = T extends null | undefined ? never : T; + +type CleanedObject = { + [K in keyof T as ExcludeNullOrUndefined extends never + ? never + : K]: ExcludeNullOrUndefined; +}; + +const notNullOrUndefined = (value: T): value is ExcludeNullOrUndefined => + value !== null && value !== undefined; + +export const cleanParams = (params: T): CleanedObject => { + const cleanedEntries = Object.entries(params).filter(([, value]) => + notNullOrUndefined(value) + ); + return Object.fromEntries(cleanedEntries) as CleanedObject; +}; + +export const toQueryString = (params: Record): string => + Object.entries(params) + .map( + ([key, value]) => + `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}` + ) + .join("&");