2024-06-07 20:17:03 +03:00
|
|
|
/**
|
|
|
|
* Module: Utility functions.
|
|
|
|
*/
|
|
|
|
|
2025-02-10 01:32:16 +03:00
|
|
|
import { AxiosError, AxiosHeaderValue, AxiosResponse, isAxiosError } from 'axios';
|
2025-01-26 22:24:34 +03:00
|
|
|
import { toast } from 'react-toastify';
|
2024-06-07 20:17:03 +03:00
|
|
|
|
2025-01-26 22:24:34 +03:00
|
|
|
import { information, prompts } from './labels';
|
2024-06-07 20:17:03 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks if arguments is Node.
|
|
|
|
*/
|
|
|
|
export function assertIsNode(e: EventTarget | null): asserts e is Node {
|
|
|
|
if (e === null || !('nodeType' in e)) {
|
|
|
|
throw new TypeError(`Expected 'Node' but received '${e?.constructor.name ?? 'null'}'`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Wrapper class for generalized text matching.
|
|
|
|
*
|
|
|
|
* If possible create regexp, otherwise use symbol matching.
|
|
|
|
*/
|
|
|
|
export class TextMatcher {
|
|
|
|
protected query: RegExp | string;
|
|
|
|
|
|
|
|
constructor(query: string, isPlainText?: boolean, isCaseSensitive?: boolean) {
|
|
|
|
if (isPlainText) {
|
|
|
|
query = query.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
this.query = new RegExp(query, isCaseSensitive ? '' : 'i');
|
2024-08-06 14:38:10 +03:00
|
|
|
} catch (_exception: unknown) {
|
2024-06-07 20:17:03 +03:00
|
|
|
this.query = query;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
test(text: string): boolean {
|
|
|
|
if (typeof this.query === 'string') {
|
2024-08-06 14:38:10 +03:00
|
|
|
return text.includes(this.query);
|
2024-06-07 20:17:03 +03:00
|
|
|
} else {
|
|
|
|
return !!text.match(this.query);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-11 14:24:37 +03:00
|
|
|
/**
|
2024-08-19 19:15:21 +03:00
|
|
|
* Truncate text to last word up to max symbols. Add ellipsis if truncated.
|
2024-08-11 14:24:37 +03:00
|
|
|
*/
|
2024-08-19 19:15:21 +03:00
|
|
|
export function truncateToLastWord(text: string, maxSymbols: number): string {
|
2024-08-11 14:24:37 +03:00
|
|
|
if (text.length <= maxSymbols) {
|
|
|
|
return text;
|
|
|
|
}
|
|
|
|
const trimmedText = text.slice(0, maxSymbols);
|
|
|
|
const lastSpaceIndex = trimmedText.lastIndexOf(' ');
|
|
|
|
if (lastSpaceIndex === -1) {
|
|
|
|
return trimmedText + '...';
|
|
|
|
}
|
|
|
|
return trimmedText.slice(0, lastSpaceIndex) + '...';
|
|
|
|
}
|
|
|
|
|
2024-08-19 19:15:21 +03:00
|
|
|
/**
|
|
|
|
* Truncate text to max symbols. Add ellipsis if truncated.
|
|
|
|
*/
|
|
|
|
export function truncateToSymbol(text: string, maxSymbols: number): string {
|
|
|
|
if (text.length <= maxSymbols) {
|
|
|
|
return text;
|
|
|
|
}
|
|
|
|
const trimmedText = text.slice(0, maxSymbols);
|
|
|
|
return trimmedText + '...';
|
|
|
|
}
|
|
|
|
|
2024-06-07 20:17:03 +03:00
|
|
|
/**
|
|
|
|
* Check if Axios response is html.
|
|
|
|
*/
|
|
|
|
export function isResponseHtml(response?: AxiosResponse) {
|
|
|
|
if (!response) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const header = response.headers['content-type'] as AxiosHeaderValue;
|
|
|
|
if (!header) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (typeof header === 'number' || typeof header === 'boolean') {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
|
|
|
|
return header.includes('text/html');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Convert base64 string to Blob uint8.
|
|
|
|
*/
|
|
|
|
export function convertBase64ToBlob(base64String: string): Uint8Array {
|
|
|
|
const arr = base64String.split(',');
|
|
|
|
const bstr = atob(arr[1]);
|
|
|
|
let n = bstr.length;
|
|
|
|
const uint8Array = new Uint8Array(n);
|
|
|
|
while (n--) {
|
|
|
|
uint8Array[n] = bstr.charCodeAt(n);
|
|
|
|
}
|
|
|
|
return uint8Array;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Prompt user of confirming discarding changes before continue.
|
|
|
|
*/
|
|
|
|
export function promptUnsaved(): boolean {
|
|
|
|
return window.confirm(prompts.promptUnsaved);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Toggle tristate flag: undefined - true - false.
|
|
|
|
*/
|
|
|
|
export function toggleTristateFlag(prev: boolean | undefined): boolean | undefined {
|
|
|
|
if (prev === undefined) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return prev ? false : undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Toggle tristate color: gray - green - red .
|
|
|
|
*/
|
|
|
|
export function tripleToggleColor(value: boolean | undefined): string {
|
|
|
|
if (value === undefined) {
|
|
|
|
return 'clr-text-controls';
|
|
|
|
}
|
2024-12-16 23:51:31 +03:00
|
|
|
return value ? 'text-ok-600' : 'text-warn-600';
|
2024-06-07 20:17:03 +03:00
|
|
|
}
|
2024-08-18 12:55:41 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Extract error message from error object.
|
|
|
|
*/
|
|
|
|
export function extractErrorMessage(error: Error | AxiosError): string {
|
2025-02-10 01:32:16 +03:00
|
|
|
if (isAxiosError(error)) {
|
2024-08-18 12:55:41 +03:00
|
|
|
if (error.response && error.response.status === 400) {
|
|
|
|
const data = error.response.data as Record<string, unknown>;
|
|
|
|
const keys = Object.keys(data);
|
|
|
|
if (keys.length === 1) {
|
|
|
|
const value = data[keys[0]];
|
|
|
|
if (typeof value === 'string') {
|
|
|
|
return `${keys[0]}: ${value}`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return error.message;
|
|
|
|
}
|
2024-08-22 22:41:29 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Convert array of objects to CSV Blob.
|
|
|
|
*/
|
|
|
|
export function convertToCSV(targetObj: object[]): Blob {
|
|
|
|
if (!targetObj || targetObj.length === 0) {
|
|
|
|
return new Blob([], { type: 'text/csv;charset=utf-8;' });
|
|
|
|
}
|
|
|
|
const separator = ',';
|
|
|
|
const keys = Object.keys(targetObj[0]);
|
|
|
|
|
|
|
|
const csvContent =
|
|
|
|
keys.join(separator) +
|
|
|
|
'\n' +
|
|
|
|
(targetObj as Record<string, string | Date | number>[])
|
|
|
|
.map(item => {
|
|
|
|
return keys
|
|
|
|
.map(k => {
|
|
|
|
let cell = item[k] === null || item[k] === undefined ? '' : item[k];
|
|
|
|
cell = cell instanceof Date ? cell.toLocaleString() : cell.toString().replace(/"/g, '""');
|
|
|
|
if (cell.search(/("|,|\n)/g) >= 0) {
|
|
|
|
cell = `"${cell}"`;
|
|
|
|
}
|
|
|
|
return cell;
|
|
|
|
})
|
|
|
|
.join(separator);
|
|
|
|
})
|
|
|
|
.join('\n');
|
|
|
|
|
|
|
|
return new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
|
|
}
|
2025-01-26 22:24:34 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Generates a QR code for the current page.
|
|
|
|
*/
|
|
|
|
export function generatePageQR(): string {
|
|
|
|
const currentRef = window.location.href;
|
|
|
|
return currentRef.includes('?') ? currentRef + '&qr' : currentRef + '?qr';
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Copies sharable link to the current page.
|
|
|
|
*/
|
|
|
|
export function sharePage() {
|
|
|
|
const currentRef = window.location.href;
|
|
|
|
const url = currentRef.includes('?') ? currentRef + '&share' : currentRef + '?share';
|
|
|
|
navigator.clipboard
|
|
|
|
.writeText(url)
|
|
|
|
.then(() => toast.success(information.linkReady))
|
|
|
|
.catch(console.error);
|
|
|
|
}
|
2025-02-11 20:56:11 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove html tags from target string.
|
|
|
|
*/
|
|
|
|
export function removeTags(target?: string): string {
|
|
|
|
if (!target) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
return target.toString().replace(/(<([^>]+)>)/gi, '');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generate HTML wrapper for control description including hotkey.
|
|
|
|
*/
|
|
|
|
export function prepareTooltip(text: string, hotkey?: string) {
|
|
|
|
return hotkey ? `<b>[${hotkey}]</b><br/>${text}` : text;
|
|
|
|
}
|