diff --git a/.vscode/settings.json b/.vscode/settings.json index a8ab85f..c5c4663 100755 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { "cSpell.words": [ - "день" + "день", + "Saub" ], "liveServer.settings.port": 5501 } \ No newline at end of file diff --git a/src/components/Table/Columns/date.tsx b/src/components/Table/Columns/date.tsx index a401a04..ed3428a 100755 --- a/src/components/Table/Columns/date.tsx +++ b/src/components/Table/Columns/date.tsx @@ -5,6 +5,11 @@ import { DatePickerWrapper, getObjectByDeepKey } from '..' import { DatePickerWrapperProps } from '../DatePickerWrapper' import { formatDate, isRawDate } from '@utils' +/** + * Фабрика методов сортировки столбцов для данных типа **Дата** + * @param key Ключ столбца + * @returns Метод сортировки + */ export const makeDateSorter = (key: Key): SorterMethod => (a, b) => { const vA = a ? getObjectByDeepKey(key, a) : null const vB = b ? getObjectByDeepKey(key, b) : null @@ -16,6 +21,17 @@ export const makeDateSorter = (key: Key): SorterMethod => return (new Date(vA)).getTime() - (new Date(vB)).getTime() } +/** + * Фабрика объектов-столбцов для компонента `Table` для работы с данными типа **Дата** + * + * @param title Название столбца + * @param key Ключ столбца + * @param utc Конвертировать ли дату в UTC + * @param format Формат отображения даты + * @param other Дополнительные опции столбца + * @param pickerOther Опции компонента селектора даты + * @returns Объект-столбец для работы с данными типа **Дата** + */ export const makeDateColumn = ( title: ReactNode, key: string, diff --git a/src/components/Table/DatePickerWrapper.tsx b/src/components/Table/DatePickerWrapper.tsx index 8aaa9ba..188d985 100755 --- a/src/components/Table/DatePickerWrapper.tsx +++ b/src/components/Table/DatePickerWrapper.tsx @@ -6,8 +6,11 @@ import moment, { Moment } from 'moment' import { defaultFormat } from '@utils' export type DatePickerWrapperProps = PickerDateProps & { + /** Значение селектора */ value?: Moment, + /** Метод вызывается при изменений даты */ onChange?: (date: Moment | null) => any + /** Конвертировать ли значение в UTC */ isUTC?: boolean } diff --git a/src/components/Table/DateRangeWrapper.tsx b/src/components/Table/DateRangeWrapper.tsx index 4ad140f..c9246c5 100755 --- a/src/components/Table/DateRangeWrapper.tsx +++ b/src/components/Table/DateRangeWrapper.tsx @@ -9,31 +9,41 @@ import { defaultFormat } from '@utils' const { RangePicker } = DatePicker export type DateRangeWrapperProps = RangePickerSharedProps & { - value?: RangeValue, + /** Значение селектора в виде массива из 2 элементов (от, до) */ + value?: RangeValue + /** Конвертировать ли значения в UTC */ isUTC?: boolean + /** Разрешить сброс значения селектора */ allowClear?: boolean } +/** + * Подготавливает значения к передаче в селектор + * + * @param value Массиз из 2 дат + * @param isUTC Конвертировать ли значения в UTC + * @returns Подготовленные даты + */ const normalizeDates = (value?: RangeValue, isUTC?: boolean): RangeValue => { - if (!value) return [null, null] - return [ - value[0] ? (isUTC ? moment.utc(value[0]).local() : moment(value[0])) : null, - value[1] ? (isUTC ? moment.utc(value[1]).local() : moment(value[1])) : null, - ] + if (!value) return [null, null] + return [ + value[0] ? (isUTC ? moment.utc(value[0]).local() : moment(value[0])) : null, + value[1] ? (isUTC ? moment.utc(value[1]).local() : moment(value[1])) : null, + ] } -export const DateRangeWrapper = memo(({ value, isUTC, allowClear = false, ...other }) => ( - +export const DateRangeWrapper = memo(({ value, isUTC, allowClear, ...other }) => ( + )) export default DateRangeWrapper diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index c9ecbbe..e1b7f8c 100755 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -1,6 +1,6 @@ import { Key, memo, useCallback, useEffect, useState } from 'react' import { ColumnGroupType, ColumnType } from 'antd/lib/table' -import { Table as RawTable, TableProps } from 'antd' +import { Table as RawTable, TableProps as RawTableProps } from 'antd' import { RenderMethod } from './Columns' import { tryAddKeys } from './EditableTable' @@ -14,16 +14,28 @@ export type BaseTableColumn = ColumnGroupType | ColumnType export type TableColumn = OmitExtends, TableColumnSettings> export type TableColumns = TableColumn[] -export type TableContainer = TableProps & { +export type TableProps = RawTableProps & { + /** Массив колонок таблицы с настройками (описаны в `TableColumnSettings`) */ columns: TableColumn[] + /** Название таблицы для сохранения настроек */ tableName?: string + /** Отображать ли кнопку настроек */ showSettingsChanger?: boolean } export interface DataSet { - [k: Key]: DataSet | T | D + [k: Key]: DataSet | T | D } +/** + * Получить значение из объекта по составному ключу + * + * Составной ключ имеет вид: `<поле 1>[.<поле 2>...]` + * + * @param key Составной ключ + * @param data Объект из которого будет полученно значение + * @returns Значение, найденное по ключу, либо `undefined` + */ export const getObjectByDeepKey = (key: Key | undefined, data: DataSet): T | undefined => { if (!key) return undefined const parts = String(key).split('.') @@ -36,36 +48,44 @@ export const getObjectByDeepKey = (key: Key | undefined, data: DataSet): return out as T } +/** + * Фабрика обёрток render-функций ячеек с поддержкой составных ключей + * @param key Составной ключ + * @param render Стандартная render-функция + * @returns Обёрнутая render-функция + */ export const makeColumnRenderWrapper = >(key: Key | undefined, render: RenderMethod | undefined): RenderMethod => (_: any, dataset: T, index: number) => { const renderFunc: RenderMethod = typeof render === 'function' ? render : (record) => String(record) return renderFunc(getObjectByDeepKey(key, dataset), dataset, index) } - -const applyColumnWrappers = >(columns: BaseTableColumn[]): BaseTableColumn[] => { - return columns.map((column) => { - if ('children' in column) { - return { - ...column, - children: applyColumnWrappers(column.children), - } - } +/** + * Применяет необходимые обёртки ко всем столбцам таблицы + * @param columns Исходные столбцы + * @returns Обёрнутые столбцы + */ +const applyColumnWrappers = >(columns: TableColumns): TableColumns => columns.map((column) => { + if ('children' in column) { return { ...column, - render: makeColumnRenderWrapper(column.key, column.render), + children: applyColumnWrappers(column.children), } - }) -} + } + return { + ...column, + render: makeColumnRenderWrapper(column.key, column.render), + } +}) -function _Table>({ columns, dataSource, tableName, showSettingsChanger, ...other }: TableContainer) { +function _Table>({ columns, dataSource, tableName, showSettingsChanger, ...other }: TableProps) { const [newColumns, setNewColumns] = useState[]>([]) const [settings, setSettings] = useState({}) const onSettingsChanged = useCallback((settings?: TableSettings | null) => { if (tableName) setTableSettings(tableName, settings) - setSettings(settings ?? {}) + setSettings(settings || {}) }, [tableName]) useEffect(() => setSettings(tableName ? getTableSettings(tableName) : {}), [tableName]) @@ -92,6 +112,13 @@ function _Table>({ columns, dataSource, tableName, showSe ) } +/** + * Обёртка над компонентом таблицы AntD + * + * Особенности: + * * Поддержка составных ключей столбцов + * * Работа с настройками столбцов таблицы + */ export const Table = memo(_Table) as typeof _Table export default Table diff --git a/src/components/Table/TimePickerWrapper.tsx b/src/components/Table/TimePickerWrapper.tsx index de748d8..e47848a 100644 --- a/src/components/Table/TimePickerWrapper.tsx +++ b/src/components/Table/TimePickerWrapper.tsx @@ -6,8 +6,11 @@ import { defaultTimeFormat, momentToTime, timeToMoment } from '@utils' import { TimeDto } from '@api' export type TimePickerWrapperProps = Omit, 'onChange'> & { + /** Текущее значение */ value?: TimeDto, + /** Метод вызывается при изменений времени */ onChange?: (date: TimeDto | null) => any + /** Конвертировать ли время в UTC */ isUTC?: boolean } diff --git a/src/components/Table/index.tsx b/src/components/Table/index.tsx index 3f4eca8..bb9a292 100755 --- a/src/components/Table/index.tsx +++ b/src/components/Table/index.tsx @@ -17,6 +17,13 @@ export type PaginationContainer = { items?: T[] | null } +/** + * Генерирует объект пагинации для компонента `Table` из данных от сервисов + * + * @param сontainer данные от сервиса + * @param other Дополнительные поля (передаются в объект напрямую в приоритете) + * @returns Объект пагинации + */ export const makePaginationObject = (сontainer: PaginationContainer, other: M) => ({ ...other, pageSize: сontainer.take, diff --git a/src/components/views/CompanyView.tsx b/src/components/views/CompanyView.tsx index db012a5..647100a 100755 --- a/src/components/views/CompanyView.tsx +++ b/src/components/views/CompanyView.tsx @@ -9,6 +9,7 @@ export type CompanyViewProps = { company?: CompanyDto } +/** Компонент для отображения информации о компании */ export const CompanyView = memo(({ company }) => company ? ( diff --git a/src/components/views/PermissionView.tsx b/src/components/views/PermissionView.tsx index 7708be5..9c185a7 100755 --- a/src/components/views/PermissionView.tsx +++ b/src/components/views/PermissionView.tsx @@ -8,6 +8,7 @@ export type PermissionViewProps = { info?: PermissionDto } +/** Компонент для отображения информации о разрешении */ export const PermissionView = memo(({ info }) => info ? ( diff --git a/src/components/views/RoleView.tsx b/src/components/views/RoleView.tsx index 8974419..66c0ad3 100755 --- a/src/components/views/RoleView.tsx +++ b/src/components/views/RoleView.tsx @@ -9,6 +9,7 @@ export type RoleViewProps = { role?: UserRoleDto } +/** Компонент для отображения информации о роли */ export const RoleView = memo(({ role }) => { if (!role) return ( - ) diff --git a/src/components/views/TelemetryView.tsx b/src/components/views/TelemetryView.tsx index 8758353..cec6093 100755 --- a/src/components/views/TelemetryView.tsx +++ b/src/components/views/TelemetryView.tsx @@ -19,6 +19,12 @@ export const lables: Record = { spinPlcVersion: 'Версия Спин Мастер', } +/** + * Строит название для телеметрии + * + * @param telemetry Объект телеметрии + * @returns Название + */ export const getTelemetryLabel = (telemetry?: TelemetryDto) => `${telemetry?.id ?? '-'} / ${telemetry?.info?.deposit ?? '-'} / ${telemetry?.info?.cluster ?? '-'} / ${telemetry?.info?.well ?? '-'}` @@ -26,6 +32,7 @@ export type TelemetryViewProps = { telemetry?: TelemetryDto } +/** Компонент для отображения информации о телеметрии */ export const TelemetryView = memo(({ telemetry }) => telemetry?.info ? ( & { user?: UserDto } +/** Компонент для отображения информации о пользователе */ export const UserView = memo(({ user, ...other }) => user ? ( , HTMLSpanElement> } +/** + * Получить название скважины + * @param well Объект с данными скважины + * @returns Название скважины + */ export const getWellTitle = (well: WellDto) => `${well.deposit || '-'} / ${well.cluster || '-'} / ${well.caption || '-'}` +/** Компонент для отображения информации о скважине */ export const WellView = memo(({ well, iconProps, labelProps, ...other }) => well ? ( diff --git a/src/components/views/WirelineView.tsx b/src/components/views/WirelineView.tsx index 5ea9bd0..be0b3f9 100644 --- a/src/components/views/WirelineView.tsx +++ b/src/components/views/WirelineView.tsx @@ -21,6 +21,7 @@ export type WirelineViewProps = TooltipProps & { buttonProps?: ButtonProps } +/** Компонент для отображения информации о талевом канате */ export const WirelineView = memo(({ wireline, buttonProps, ...other }) => ( { const maxTarget = Math.max(...data.operations?.map((op) => op.operationValue?.targetValue || 0)) const uniqueOps = data.operations?.map((op) => op.value || 0).filter(unique) const value = uniqueOps.reduce((out, op) => out + op, 0) / uniqueOps.length * 3 / 2 - setYDomain(pretify(Math.max(maxTarget, value))) + setYDomain(prettify(Math.max(maxTarget, value))) }, [data]) useEffect(() => { diff --git a/src/pages/Well/WellOperations/Tvd/index.jsx b/src/pages/Well/WellOperations/Tvd/index.jsx index f3357f6..ba0a19b 100644 --- a/src/pages/Well/WellOperations/Tvd/index.jsx +++ b/src/pages/Well/WellOperations/Tvd/index.jsx @@ -8,7 +8,7 @@ import { useTopRightBlock, useWell } from '@asb/context' import { D3Chart } from '@components/d3' import LoaderPortal from '@components/LoaderPortal' import { invokeWebApiWrapperAsync } from '@components/factory' -import { formatDate, fractionalSum, withPermissions, getOperations, pretify } from '@utils' +import { formatDate, fractionalSum, withPermissions, getOperations, prettify } from '@utils' import TLPie from './TLPie' import TLChart from './TLChart' @@ -180,7 +180,7 @@ const Tvd = memo(({ well: givenWell, title, ...other }) => { .map(([_, ops]) => Math.max(...ops.map((op) => op.depth).filter(Boolean))) .filter(Boolean) ) - const minValue = pretify(maxValue) + const minValue = prettify(maxValue) return { date: { diff --git a/src/utils/filters/columnFilters.ts b/src/utils/filters/columnFilters.ts index dcf0e2d..6965521 100644 --- a/src/utils/filters/columnFilters.ts +++ b/src/utils/filters/columnFilters.ts @@ -1,12 +1,30 @@ +import { getObjectByDeepKey } from "@asb/components/Table" + +/** + * Фабрика методов фильтрации строк таблицы по столбцу с текстом + * @param key Составной ключ столбца + * @returns Метод фильтрации + */ export const makeTextOnFilter = (key: string) => - (value: string, record?: Record) => String(record?.[key]).startsWith(value) + (value: string, record?: Record) => record && String(getObjectByDeepKey(key, record)).startsWith(value) +/** + * Фабрика методов фильтрации строк таблицы по столбцу с массивами + * @param key Составной ключ столбца + * @returns Метод фильтрации + */ export const makeArrayOnFilter = (key: string) => - (value: string, record?: Record) => (!value && (record?.[key]?.length ?? 0) <= 0) || record?.[key]?.includes(value) - -export const makeObjectOnFilter = (field: string, key: string) => - (value: string, record?: Record>) => String(record?.[field]?.[key]).startsWith(value) + (value: string, record?: Record) => record && ( + (!value && (getObjectByDeepKey(key, record)?.length ?? 0) <= 0) || + getObjectByDeepKey(key, record)?.includes(value) + ) +/** + * Создаёт список значений для фильтрации текстовых столбцов + * @param array Массив значений + * @param keys Массив ключей + * @returns Список значений + */ export const makeTextFilters = (array: Record[], keys: string[]) => { const filters: string[][] = Array(keys.length) diff --git a/src/utils/filters/index.ts b/src/utils/filters/index.ts index cb2982b..26e6062 100644 --- a/src/utils/filters/index.ts +++ b/src/utils/filters/index.ts @@ -1,3 +1,11 @@ export * from './columnFilters' +/** + * Проверяет значение на уникальность в массиве + * + * @param value Проверяемое значение + * @param index Индекс проверяемого значения в массиве + * @param self Массив, содержащий значение + * @returns Является ли значение уникальным или первым + */ export const unique = (value: T, index: number, self: T[]) => self.indexOf(value) === index diff --git a/src/utils/functions/arrayOrDefault.ts b/src/utils/functions/arrayOrDefault.ts index eed23e8..636faf2 100644 --- a/src/utils/functions/arrayOrDefault.ts +++ b/src/utils/functions/arrayOrDefault.ts @@ -1,10 +1,10 @@ /** - * Возвращает - * + * Гарантированно возвращает массив нужного типа + * * @param arr Входящие данные * @param def Значение по-умолчанию - * - * @returns Если `arr` - массив будет возвращено оно, иначе `def` + * + * @returns Если входящие данные - массив будут возвращены они, иначе значение из `def` */ export const arrayOrDefault = (arr: unknown, def: T[] = []): T[] => arr instanceof Array ? arr : def diff --git a/src/utils/functions/chart.tsx b/src/utils/functions/chart.tsx index 93c677c..42eb574 100644 --- a/src/utils/functions/chart.tsx +++ b/src/utils/functions/chart.tsx @@ -3,6 +3,12 @@ import { AntdIconProps } from '@ant-design/icons/lib/components/AntdIcon' import { BaseDataType, ChartDataset } from '@components/d3' +/** + * Фабрика методов-оптимизаторов для массивов точек перед выводом на график + * + * @param isEquals Метод сравнения элементов на равенство + * @returns Метод вырезки идентичных элементов (по результатам работы переданного метода) + */ export const makePointsOptimizator = (isEquals: (a: DataType, b: DataType) => boolean) => (points: DataType[]) => { if (!Array.isArray(points) || points.length < 3) return points @@ -16,6 +22,22 @@ export const makePointsOptimizator = (isEquals: ( export type TouchType = 'all' | 'x' | 'y' +/** + * Получает расстояние между точками в зависимости от типа касания + * + * Если тип касания 'x', то вернёт модуль разницы абсцисс + * + * Если тип касания 'y', то вернёт модуль разницы ординат + * + * Иначе вернёт корень суммы квадратов разностей координат точек + * + * @param x1 Абсцисса первой точки + * @param y1 Ордината первой точки + * @param x2 Абсцисса второй точки + * @param y2 Ордината второй точки + * @param type Тип касания + * @returns Расстояние между точками + */ export const getDistance = (x1: number, y1: number, x2: number, y2: number, type: TouchType = 'all') => { if (type === 'x') return Math.abs(x1 - x2) if (type === 'y') return Math.abs(y1 - y2) @@ -23,6 +45,13 @@ export const getDistance = (x1: number, y1: number, x2: number, y2: number, type return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)) } +/** + * Возвращает иконку графика в зависимости от типа + * + * @param chart График, у которого будет проверен тип + * @param options Дополнительные опции иконки + * @returns Элемент иконки + */ export const getChartIcon = (chart: ChartDataset, options?: Omit) => { let Icon switch (chart.type) { diff --git a/src/utils/functions/datetime.ts b/src/utils/functions/datetime.ts index f4edc4e..f409d9e 100644 --- a/src/utils/functions/datetime.ts +++ b/src/utils/functions/datetime.ts @@ -16,10 +16,22 @@ export enum timeInS { week = day * 7, } +/** + * Проверка значения на возможность преобразования к дате + * + * @param value Проверяемое значение + * @returns Является ли значение потенциальной датой + */ export function isRawDate(value: unknown): value is RawDate { return !isNaN(Date.parse(String(value))) } +/** + * Проверка значения на возможность преобразования к `TimeDto` + * + * @param value Проверяемое значение + * @returns Является ли значение потенциальным объектом `TimeDto` + */ export function isTime(value: unknown): value is TimeDto { if (!value || typeof value !== 'object') return false @@ -27,18 +39,40 @@ export function isTime(value: unknown): value is TimeDto { return ['hour', 'minute', 'second'].every((key) => keys.includes(key)) } +/** + * Форматировать значение как дату в строку + * + * @param date Форматируемое значение + * @param utc Преобразовывать ли к UTC + * @param format Формат вывода + * @returns Форматированная дата в строке + */ export const formatDate = (date: unknown, utc: boolean = false, format: string = defaultFormat) => { if (!isRawDate(date)) return null const out = utc ? moment.utc(date).local() : moment(date) return out.format(format) } +/** + * Форматировать значение как время в строку + * + * @param time Форматируемое значение + * @param utc Преобразовывать ли к UTC + * @param format Формат вывода + * @returns Форматированное время в строке + */ export const formatTime = (time: unknown, utc: boolean = false, format: string = defaultTimeFormat) => { if(!isTime(time)) return const out = timeToMoment(time, utc, format) return out.format(format) } +/** + * Привести секунды к строке периоду + * + * @param time Указанное кол-во секунд + * @returns Форматированная строка период + */ export const periodToString = (time?: number) => { if (!time || time <= 0) return '00:00:00' const days = Math.floor(time / timeInS.day) @@ -54,11 +88,26 @@ export const periodToString = (time?: number) => { return `${days > 0 ? days : ''} ${toFixed(hours)}:${toFixed(minutes)}:${toFixed(seconds)}` } +/** + * Вычислить кол-во дней между двумя датами + * + * @param start Левая дата + * @param end Правая дата + * @returns если оба аргумента потенциальны даты, то кол-во дней между ними, иначе `undefined` + */ export const calcDuration = (start: unknown, end: unknown): number | undefined => { if (!isRawDate(start) || !isRawDate(end)) return undefined return (+new Date(end) - +new Date(start)) * timeInS.millisecond / timeInS.day } +/** + * Сдвинуть дату на указанное время + * + * @param date Исходная дата + * @param value Коэффициент сдвига + * @param type Тип сдвига (день, час, минут и т.д.) + * @returns Смещённая дата + */ export const fractionalSum = (date: unknown, value: number, type: keyof typeof timeInS): RawDate | null => { if (!isRawDate(date) || !timeInS[type] || isNaN(value ?? NaN)) return null const d = new Date(date) @@ -86,17 +135,41 @@ export const rawTimezones = Object.freeze({ export type TimezoneId = keyof typeof rawTimezones +/** + * Проверяет, является ли переданное значение корректным ID часовой зоны + * + * @param value Проверяемое значение + * @returns Является ли переданное значение корректным ID часовой зоны + */ export const isTimezoneId = (value: unknown): value is TimezoneId => !!value && String(value) in rawTimezones +/** + * Ищет часовую зону для переданной телеметрии + * @param value Данные телеметрии + * @returns Название часовой зоны + */ export const findTimezoneId = (value: SimpleTimezoneDto): TimezoneId => (isTimezoneId(value.timezoneId) && value.timezoneId) || (Object.keys(rawTimezones) as TimezoneId[]).find(id => rawTimezones[id] === value.hours) as TimezoneId +/** + * Приводит `TimeDto`-объект к `Moment`-объекту + * + * @param time `TimeDto`-объект + * @param isUtc Приводить ли к UTC + * @param format Формат для обработки + * @returns `Moment`-объект + */ export const timeToMoment = (time?: TimeDto | null, isUtc?: boolean, format: string = defaultTimeFormat): Moment => { const input = `${time?.hour ?? 0}:${time?.minute ?? 0}:${time?.second ?? 0}` return isUtc ? moment.utc(input, format).local() : moment(input, format) } +/** + * Приводит `Moment`-объект к `TimeDto`-объекту + * @param time `Moment`-объект + * @returns `TimeDto`-объекту + */ export const momentToTime = (time?: Moment | null): TimeDto => ({ hour: time?.hour() ?? 0, minute: time?.minute() ?? 0, diff --git a/src/utils/functions/numbers.tsx b/src/utils/functions/numbers.tsx index 6efb69c..949e44e 100644 --- a/src/utils/functions/numbers.tsx +++ b/src/utils/functions/numbers.tsx @@ -1,10 +1,18 @@ import { ReactNode } from 'react' +/** + * Форматирует число по заданным параметрам + * + * @param number Форматируемое число + * @param def Вывод по-умолчанию + * @param fixed Длина числа после форматирования + * @returns Строка - форматированное число + */ export const getPrecision = (number: number, def: string = '-', fixed: number = 2): string => Number.isFinite(number) ? number.toFixed(fixed) : def /** * Генерирует функцию ограничения значения в заданном диапазоне - * + * * @param min Минимальное значение * @param max Максимальное значение * @returns Функция, ограничивающая значение в диапазоне [`min`; `max`] @@ -17,15 +25,20 @@ export const limitValue = (min: T, max: T) => (value: T) => { /** * Генерирует массив чисел в заданном диапазоне - * + * * @param end Конечное значение * @param start Начальное значение - * + * * @returns Массив чисел в диапазоне от `start` до `end` */ export const range = (end: number, start: number = 0) => Array(end - start).fill(undefined).map((_, i) => start + i) -export const pretify = (n: number): number | null => { +/** + * Округляет число до ближайшего красивого + * @param n Округляемое число + * @returns Округлённое число + */ +export const prettify = (n: number): number | null => { if (!Number.isFinite(n)) return null let i = 0 for (; Math.abs(n) >= 100; i++) n /= 10 diff --git a/src/utils/functions/objects.ts b/src/utils/functions/objects.ts index 3435196..350d090 100644 --- a/src/utils/functions/objects.ts +++ b/src/utils/functions/objects.ts @@ -1,21 +1,21 @@ /** - * Копирует данные в глубину. - * + * Копирует данные с максимальной глубиной + * * @remarks - * При копированиий объектов, содержащих функций может возникнуть исключение. + * При копирований объектов, содержащих функций может возникнуть исключение. * Не предназначено для копирования функций. - * + * * @param data Копируемые данные - * @returns Полная копия `data` + * @returns Полная копия исходных данных */ export const deepCopy = (data: T): T => JSON.parse(JSON.stringify(data ?? null)) /** - * Маппинг полей объекта - * - * @param data Входящие данные - * @param handler Обработчик - * + * Аналог функции `Array.prototype.map`, но для работы с объектами + * + * @param data Исходный объект + * @param handler Метод-обработчик + * * @returns Объект с обработанными полями */ export const wrapValues = (data: Record, handler: (data: T, key: string, object: Record) => R): Record => diff --git a/src/utils/functions/permissions.tsx b/src/utils/functions/permissions.tsx index 347ad53..44883d1 100644 --- a/src/utils/functions/permissions.tsx +++ b/src/utils/functions/permissions.tsx @@ -12,10 +12,22 @@ export type ServiceName = string export type ServiceRequestType = 'get' | 'edit' | 'delete' export type PermissionRequest = `${ServiceName}.${ServiceRequestType}` +/** + * Проверка соответствует ли значение типу `ServiceRequestType` + * + * @param value Проверяемое значение + * @returns Является ли значение объектом типа `ServiceRequestType` + */ export function isRequestType(value: string): value is ServiceRequestType { return ['get', 'edit', 'delete'].includes(value) } +/** + * Генерация объекта, содержащего информацию о наличии или отсутствия перечисленных разрешений + * + * @param values Список разрешений + * @returns Объект с информацией о разрешениях + */ export const getPermissions = (...values: PermissionRequest[]) => { const permissions: Record>> = {} values.forEach((key) => { @@ -27,6 +39,13 @@ export const getPermissions = (...values: PermissionRequest[]) => { return permissions } +/** + * Проверка наличия у пользователя разрешения или списка разрешений + * + * @param permission Разрешение или список разрешений + * @param userPermissions Список разрешений пользователя (если не указано, будут получены из локального хранилища) + * @returns `true` если все разрешения присутствуют, иначе `false` + */ export const hasPermission = (permission?: Permission | Permission[], userPermissions?: Permission[]): boolean => { if (!Array.isArray(permission) && typeof permission !== 'string') return true @@ -36,6 +55,13 @@ export const hasPermission = (permission?: Permission | Permission[], userPermis return permission.every((perm) => userPerms.includes(perm)) } +/** + * Проверка доступности секции сайта для посещения пользователем + * + * @param section Секция сайта + * @param userPermission Разрешения пользователя + * @returns `true` если все разрешения присутствуют, иначе `false` + */ const sectionAvailable = (section: PermissionRecord, userPermission: Permission[]) => { for (const child of Object.values(section)) { if (!child) continue @@ -48,6 +74,12 @@ const sectionAvailable = (section: PermissionRecord, userPermission: Permission[ return false } +/** + * Проверка доступности URL для посещения пользователем + * @param path URL + * @param userPermissions Разрешения пользователя (если не заданы, будут получены из локального хранилища) + * @returns `true` если все разрешения присутствуют, иначе `false` + */ export const isURLAvailable = (path: string, userPermissions?: Permission[]) => { if (publicPages.includes(path)) return true @@ -95,14 +127,26 @@ export const NoAccessComponent = memo(() => getUser().login ? ( )) +/** + * HOC добавляющий проверку на наличие разрешений для отображения компонента + * @param Component Исходных компонент + * @param requirements Необходимые разрешения + * @param elseNode Компонент по-умолчанию + * @returns Обёрнутый компонент с проверкой разрешений + */ export const withPermissions =

( Component: NamedExoticComponent

| ((props: P) => ReactElement), requirements: Permission[] = [], elseNode: JSX.Element = -): PrivateComponent

=> Object.assign(memo

(function PrivateWrapper(props) { +): PrivateComponent

=> memo

(function PrivateWrapper(props) { return hasPermission(requirements) ? : elseNode -})) +}) +/** + * Получить url текущей выбранной вкладки + * + * @returns url текущей выбранной вкладки + */ export const getTabname = () => { const params = useParams() const attr = useMemo(() => params['*']?.split('/').filter(s => s)[0] ?? null, [params]) diff --git a/src/utils/functions/saubOperations.tsx b/src/utils/functions/saubOperations.tsx index 85e105c..d81a09e 100644 --- a/src/utils/functions/saubOperations.tsx +++ b/src/utils/functions/saubOperations.tsx @@ -11,10 +11,15 @@ export type SaubData = WellOperationDto & { depth?: number /** Дата */ date?: string - /** Колличество часов НПВ с начала бурения до текущего момента */ + /** Количество часов НПВ с начала бурения до текущего момента */ nptHours?: number } +/** + * Получить списки операций для конкретной скважины + * @param idWell ID скважины + * @returns Списки операций + */ export const getOperations = async (idWell: number): Promise<{ operations: WellOperationDtoPlanFactPredictBase[], plan: SaubData[] diff --git a/src/utils/functions/storage.ts b/src/utils/functions/storage.ts index 85772b7..ecc2cb9 100644 --- a/src/utils/functions/storage.ts +++ b/src/utils/functions/storage.ts @@ -16,12 +16,25 @@ export enum StorageNames { witsInfo = 'witsInfo' } +/** + * Получить массив значений из локального хранилища + * + * @param name Имя массива + * @param sep Разделитель элементов + * @returns Массив значений или `null` + */ export const getArrayFromLocalStorage = (name: string, sep: string | RegExp = ','): T[] | null => { const raw = localStorage.getItem(name) if (!raw) return null - return raw.split(sep).map(elm => elm as T) + return raw.split(sep) as T[] } +/** + * Получить объект из JSON строки в локальном хранилище + * + * @param name Имя строки + * @returns Прочитанный объект или `null` + */ export const getJSON = (name: StorageNames): T | null => { try { const raw = localStorage.getItem(name) @@ -32,6 +45,13 @@ export const getJSON = (name: StorageNames): T | null => { return null } +/** + * Записать объект в локальное хранилище, как JSON строка + * + * @param name Имя строки + * @param data Сохраняемый объект + * @returns `true` если сохранение успешно, иначе `false` + */ export const setJSON = (name: StorageNames, data: T | null): boolean => { try { localStorage.setItem(name, JSON.stringify(data)) @@ -42,8 +62,17 @@ export const setJSON = (name: StorageNames, data: T | null): boolean => { return false } +/** + * Получить информацию о пользователе из локального хранилища + * + * @returns Объект данных о пользователе + */ export const getUser = (): UserTokenDto => getJSON(StorageNames.user) || {} +/** + * Получить разрешения пользователя из локального хранилища + * @returns Список разрешений или `null` + */ export const getUserPermissions = (): Permission[] | null => { let permissions = getUser()?.permissions?.map((perm) => perm.name as string) if (!permissions) // TODO: Удалить в следующем релизе, вставлено для совместимости @@ -51,11 +80,20 @@ export const getUserPermissions = (): Permission[] | null => { return permissions || null } +/** + * Сохранить данные пользователя в локальное хранилище + * + * @param user Данные пользователя + * @returns `true` если сохранение успешно, иначе `false` + */ export const setUser = (user: UserTokenDto) => { OpenAPI.TOKEN = user.token ?? undefined - localStorage.setItem(StorageNames.user, JSON.stringify(user)) + return setJSON(StorageNames.user, user) } +/** + * Очистить данные о пользователе в локальном хранилище + */ export const removeUser = () => { localStorage.removeItem(StorageNames.userId) localStorage.removeItem(StorageNames.login) @@ -65,13 +103,26 @@ export const removeUser = () => { localStorage.removeItem(StorageNames.user) } +/** + * Получить объект настроек таблицы + * + * @param tableName Имя таблицы + * @returns Объект настроек таблицы + */ export const getTableSettings = (tableName: string): TableSettings => { const tables = getJSON(StorageNames.tableSettings) ?? {} if (!(tableName in tables)) return {} return wrapValues(tables[tableName] ?? {}, normalizeTableColumn) } -export const setTableSettings = (tableName: string, settings?: TableSettings | null): boolean => { +/** + * Сохранить объект настроек таблицы + * + * @param tableName Имя таблицы + * @param settings Объект настроек + * @returns `true` если сохранение успешно, иначе `false` + */ +export const setTableSettings = (tableName: string, settings?: TableSettings | null) => { const currentStore = getJSON(StorageNames.tableSettings) ?? {} currentStore[tableName] = wrapValues(settings ?? {}, optimizeTableColumn) return setJSON(StorageNames.tableSettings, currentStore) @@ -81,4 +132,9 @@ export type DataDashboardNNB = { } +/** + * Получить настройки панели ННБ + * + * @returns Объект настроек панели ННБ + */ export const getDashboardNNB = () => getJSON(StorageNames.dashboardNNB) diff --git a/src/utils/functions/string.ts b/src/utils/functions/string.ts index 50829da..1dbf82d 100644 --- a/src/utils/functions/string.ts +++ b/src/utils/functions/string.ts @@ -1,10 +1,18 @@ +/** + * Фабрика методов обрезки строк по словам с добавлением суффикса + * + * @param maxLength Максимальная длинна строки + * @param separator Разделитель слов в строке + * @param suffix Суффикс строки, отображаемый в случае обрезки + * @returns Метод обрезки строки + */ export const makeStringCutter = (maxLength: number = 100, separator: string = ' ', suffix: string = '...') => (comment: T): T | string => { if (!comment || typeof comment !== 'string' || comment.length <= maxLength) - return comment - if (maxLength <= suffix.length) + return comment // Обрабатываются только строки с длинной выше максимальной + if (maxLength <= suffix.length) // Если максимальная длина меньше длины суффикса вывести начало суффикса длинной `maxLength` return suffix.substring(0, maxLength) - const lastSep = comment.lastIndexOf(separator, maxLength - suffix.length) - if (lastSep < 0) + const lastSep = comment.lastIndexOf(separator, maxLength - suffix.length) // Ищем последнее разделение слов перед максимальной длинной с вычетом длины суффикса + if (lastSep < 0) // Если разделитель не найден обрезаем само слово и добавляем суффикс return comment.substring(0, maxLength - suffix.length) + suffix - return comment.substring(0, lastSep) + suffix + return comment.substring(0, lastSep) + suffix // Иначе обрезаем до разделителя и добавляем суффикс } diff --git a/src/utils/functions/svg.ts b/src/utils/functions/svg.ts index 7ec6902..c6ccd7f 100644 --- a/src/utils/functions/svg.ts +++ b/src/utils/functions/svg.ts @@ -1,4 +1,8 @@ - +/** + * Превращает SVG-элемент в `BLOB` + * @param svg SVG-элемент + * @returns `BLOB` строка + */ export const svgToDataURL = (svg: SVGSVGElement) => { const serializer = new XMLSerializer() let source = serializer.serializeToString(svg) diff --git a/src/utils/functions/table_settings.ts b/src/utils/functions/table_settings.ts index c035a4c..769db0f 100644 --- a/src/utils/functions/table_settings.ts +++ b/src/utils/functions/table_settings.ts @@ -9,20 +9,32 @@ export type TableColumnSettings = { export type TableSettings = Record export type TableSettingsStore = Record +/** + * Создаёт объект настроек таблицы исходя из массива столбцов + * @param columns Массив столбцов таблицы + * @returns Объект настроек таблицы + */ export const makeTableSettings = (columns: TableColumns): TableSettings => { const settings: TableSettings = {} columns.forEach((column) => { - if (!column.key) return + if (!column.key) return // Столбцы без ключей игнорируются const key = String(column.key) settings[key] = { columnName: key, - title: typeof column.title === 'string' ? column.title : key, - visible: column.visible ?? true, + title: typeof column.title === 'string' ? column.title : key, // В качестве заголовка невозможно использовать `ReactNode` + visible: column.visible ?? true, // По-умолчанию все столбцы видимые } }) return settings } +/** + * Объединяет несколько объектов настроек таблицы в один + * + * Приоритет объектом снижается с последнего влево + * @param settings Список объектов настроек + * @returns Совмещённый объект настроек + */ export const mergeTableSettings = (...settings: TableSettings[]): TableSettings => { const newSettings: TableSettings = {} for (const setting of settings) { @@ -36,17 +48,36 @@ export const mergeTableSettings = (...settings: TableSettings[]): TableSettings return newSettings } +/** + * Расширяет настройки столбца, полученные из хранилища + * @param column Настройки столбца + * @param name Имя столбца в случае его отсутствия в настройках + * @returns Расширенные настройки столбца + */ export const normalizeTableColumn = (column: TableColumnSettings, name?: string): TableColumnSettings => ({ ...column, columnName: column.columnName ?? name, visible: column.visible ?? true, }) +/** + * Подготавливает настройки столбца к записи в хранилище + * + * @param column Настройки столбца + * @returns Подготовленные настройки столбца + */ export const optimizeTableColumn = (column: TableColumnSettings): TableColumnSettings => ({ ...column, visible: column.visible ?? true, }) +/** + * Применяет настройки таблицы к списку столбцов + * + * @param columns Список столбцов таблицы + * @param settings Объект настройки таблицы + * @returns Список столбцов с настройками + */ export const applyTableSettings = (columns: TableColumns, settings: TableSettings): TableColumns => { let newColumns: TableColumns = columns.map((column) => ({ ...column })) newColumns = newColumns.filter((column) => { diff --git a/src/utils/hooks/functionalValue.ts b/src/utils/hooks/functionalValue.ts index 95fb552..cb4cdbb 100644 --- a/src/utils/hooks/functionalValue.ts +++ b/src/utils/hooks/functionalValue.ts @@ -3,7 +3,7 @@ import { useMemo } from 'react' import { ArgumentTypes } from '@utils/types' /** - * Значение типа может быть представлено непосредственно значением либо функцией его возвращаюшей + * Значение типа может быть представлено непосредственно значением либо функцией его возвращающей */ export type ReturnType = T extends (...args: any) => infer R ? R : any export type FunctionalValue = ReturnType | F @@ -11,8 +11,8 @@ export type FunctionalValue = ReturnType | F export const getFunctionalValue = (value: FunctionalValue) => value instanceof Function ? value : ((...args: ArgumentTypes) => value) as unknown as F /** - * Облегчает работу со значениями, которые могут быть представлены функциям. - * + * Облегчает работу со значениями, которые могут быть представлены функциям + * * @param value Значение или функция его возвращающая * @returns Функция, вызов которой вернёт искомое значение */ diff --git a/src/utils/queue.ts b/src/utils/queue.ts index cebc32e..e387511 100644 --- a/src/utils/queue.ts +++ b/src/utils/queue.ts @@ -1,10 +1,16 @@ export type TaskHandler = () => T | PromiseLike export type Queue = { + /** Метод добавления задачи в очередь */ push: (task: TaskHandler) => Promise + /** Длина очереди */ readonly length: number } +/** + * Фабрика очередей задач + * @returns Очередь задач + */ export const makeTaskQueue = (): Queue => { let pending: Promise = Promise.resolve() let count: number = 0 diff --git a/src/utils/types/index.ts b/src/utils/types/index.ts index c619b26..5431989 100644 --- a/src/utils/types/index.ts +++ b/src/utils/types/index.ts @@ -1,9 +1,9 @@ /** * Объединить типы, исключив совпадающие поля справа. - * + * * @typeParam T - Тип, передаваемый полностью * @typeParam R - Аддитивный тип - * + * * @returns Общий тип с полным `T` и несовпадающими полями из `R` */ export type OmitExtends = T & Omit