diff --git a/src/components/Private/PrivateContent.tsx b/src/components/Private/PrivateContent.tsx index 2fbecc1..404f9c7 100644 --- a/src/components/Private/PrivateContent.tsx +++ b/src/components/Private/PrivateContent.tsx @@ -1,13 +1,11 @@ import React from 'react' -import { Role, Permissions, hasAccess, PermissionMixingType } from '../../utils/permissions' +import { Role, Permission, hasPermission, isInRole } from '../../utils/PermissionService' type PrivateContentProps = { - permissions?: Permissions - roles?: Role[] - mixing?: PermissionMixingType - children?: any + roles?: Role[] | Role + permission?: Permission + children?: React.ReactElement } -export const PrivateContent: React.FC = ({ permissions, roles, mixing, children }) => { - return (hasAccess({ permissions, roles, mixing }) && children) || null -} +export const PrivateContent: React.FC = ({ permission, roles, children = null }) => + hasPermission(permission) || isInRole(roles) ? children : null diff --git a/src/components/Private/PrivateMenuItem.tsx b/src/components/Private/PrivateMenuItem.tsx index d3722e4..dc6fe32 100644 --- a/src/components/Private/PrivateMenuItem.tsx +++ b/src/components/Private/PrivateMenuItem.tsx @@ -1,15 +1,13 @@ import React from 'react' import { Menu } from 'antd' -import { hasAccess, Role, Permissions, PermissionMixingType } from '../../utils/permissions' +import { Role, Permission, hasPermission, isInRole } from '../../utils/PermissionService' type PrivateMenuItemProps = { - roles?: Role[] - permissions?: Permissions - mixing?: PermissionMixingType + roles?: Role[] | Role + permission?: Permission [props: string]: any } -export const PrivateMenuItem: React.FC = ({ roles, permissions, mixing, ...props }) => { - return hasAccess({ permissions, roles, mixing }) ? : null -} +export const PrivateMenuItem: React.FC = ({ roles, permission, mixing, ...props }) => + hasPermission(permission) || isInRole(roles) ? : null diff --git a/src/components/Private/PrivateRoute.tsx b/src/components/Private/PrivateRoute.tsx index 1f301e4..fa098bc 100644 --- a/src/components/Private/PrivateRoute.tsx +++ b/src/components/Private/PrivateRoute.tsx @@ -1,21 +1,22 @@ import React from 'react' -import { Route, Redirect } from 'react-router-dom' -import { Role, Permissions, hasAccess } from '../../utils/permissions' +import { StaticContext } from 'react-router' +import { Route, Redirect, RouteComponentProps } from 'react-router-dom' +import { Role, Permission, hasPermission, isInRole } from '../../utils/PermissionService' type PrivateRouteProps = { - permissions?: Permissions - roles: Role[] - component?: any - children?: any + roles: Role[] | Role + permission?: Permission + component?: React.ComponentType | React.ComponentType> + children?: React.ReactNode [other: string]: any } -export const PrivateRoute: React.FC = ({ permissions, roles, component, children, ...other }) => { - const available = localStorage['token'] && hasAccess({ permissions, roles }) +export const PrivateRoute: React.FC = ({ permission, roles, component, children, ...other }) => { + const available = localStorage.getItem('token') && (hasPermission(permission) || isInRole(roles)) return ( available ? children : ( )} diff --git a/src/components/Table/EditableTable.jsx b/src/components/Table/EditableTable.jsx index 9499467..1d1a2eb 100644 --- a/src/components/Table/EditableTable.jsx +++ b/src/components/Table/EditableTable.jsx @@ -38,8 +38,8 @@ export const EditableTable = ({ dataSource, onChange, // Метод вызывается со всем dataSource с измененными элементами после любого действия onRowAdd, // Метод вызывается с новой добавленной записью. Если метод не определен, то кнопка добавления строки не показывается - onRowEdit,// Метод вызывается с новой отредактированной записью. Если метод не поределен, то кнопка редактирования строки не показывается - onRowDelete,// Метод вызывается с удаленной записью. Если метод не поределен, то кнопка удаления строки не показывается + onRowEdit, // Метод вызывается с новой отредактированной записью. Если метод не поределен, то кнопка редактирования строки не показывается + onRowDelete, // Метод вызывается с удаленной записью. Если метод не поределен, то кнопка удаления строки не показывается ...otherTableProps }) => { @@ -73,10 +73,6 @@ export const EditableTable = ({ ...form.initialValues, key:newRowKeyValue } - // const newRow = { key: newRowKeyValue } - - // for (let column of columns) - // newRow[column.dataIndex] = form.initialValues?.[column.dataIndex] ?? column.initialValue const newData = [newRow, ...data] setData(newData) diff --git a/src/components/Table/index.tsx b/src/components/Table/index.tsx index c54d73e..00115e4 100644 --- a/src/components/Table/index.tsx +++ b/src/components/Table/index.tsx @@ -27,15 +27,15 @@ export const makeNumericRender = (fixed?: number) => (value: any, row: object): ) } -export const makeNumericColumnOptions = (fixed?: number, sorterKey?: string ) => ({ +export const makeNumericColumnOptions = (fixed?: number, sorterKey?: string) => ({ editable: true, initialValue: 0, - width:100, - sorter: sorterKey? makeNumericSorter(sorterKey) : null, + width: 100, + sorter: sorterKey ? makeNumericSorter(sorterKey) : null, formItemRules: [ { required: true, - message: `Введите число`, + message: 'Введите число', pattern: RegExpIsFloat, }, ], @@ -58,7 +58,7 @@ interface columnPropsOther { // массив правил валидации значений https://ant.design/components/form/#Rule formItemRules?: Rule[] // дефолтное значение при добавлении новой строки - initialValue?: string|number + initialValue?: string | number render?: (...attributes: any) => any } @@ -71,13 +71,13 @@ export const makeColumn = (title: string | ReactNode, key: string, other?: colum }) export const makeColumnsPlanFact = ( - title:string | ReactNode, - key:string | string[], + title: string | ReactNode, + key: string | string[], columsOther?: any | any[], gruopOther?: any ) => { - let keyPlanLocal = key - let keyFactLocal = key + let keyPlanLocal: string + let keyFactLocal: string if (key instanceof Array) { keyPlanLocal = key[0] @@ -87,29 +87,27 @@ export const makeColumnsPlanFact = ( keyFactLocal = key + 'Fact' } - let columsOtherLoacl : any[2] + let columsOtherLocal : any[2] if (columsOther instanceof Array) - columsOtherLoacl = [columsOther[0], columsOther[1]] + columsOtherLocal = [columsOther[0], columsOther[1]] else - columsOtherLoacl = [columsOther, columsOther] + columsOtherLocal = [columsOther, columsOther] return { title: title, ...gruopOther, children: [ - makeColumn('план', keyPlanLocal, columsOtherLoacl[0]), - makeColumn('факт', keyFactLocal, columsOtherLoacl[1]), + makeColumn('план', keyPlanLocal, columsOtherLocal[0]), + makeColumn('факт', keyFactLocal, columsOtherLocal[1]), ] } } -export const makeFilterTextMatch = (key: string | number) => (filterValue: string | number, dataItem: any) => - dataItem[key] === filterValue +export const makeFilterTextMatch = (key: string | number) => ( + (filterValue: string | number, dataItem: any) => dataItem[key] === filterValue +) -export const makeGroupColumn = (title: string, children: object[]) => ({ - title: title, - children: children, -}) +export const makeGroupColumn = (title: string, children: object[]) => ({ title, children }) export const makeTextColumn = ( title: string, diff --git a/src/pages/AdminPanel/PermissionBits.jsx b/src/pages/AdminPanel/PermissionBits.jsx deleted file mode 100644 index cabf448..0000000 --- a/src/pages/AdminPanel/PermissionBits.jsx +++ /dev/null @@ -1,123 +0,0 @@ -import { Button, Modal, Select } from 'antd' -import React, { useEffect, useState } from 'react' -import { EditableTable, makeColumn } from '../../components/Table' - -export const toHexString = (num, size) => '0x' + ('0'.repeat(size) + num.toString(16).toUpperCase()).slice(-size) - -const bitCount = 32 -const bitOptions = [...Array(bitCount).keys()].map((n) => ({ - label: toHexString(1 << n, bitCount / 4), - value: n, -})) - -const columns = [ - makeColumn('Бит', 'bit', { - // initialValue: bitOptions[0]?.value ?? '--', - input: , - // render: (value) => bitOptions.find(option => option?.value === value)?.label ?? defaultValue, - // width: 200, - // editable: true, - // formItemRules: [{ required: true, message: 'Пожалуйста, выберите бит' }], - // }), - // makeColumn('Описание', 'description', { - // width: 400, - // editable: true, - // formItemRules: [{ required: true, message: 'Пожалуйста, введите описание' }], - // }), - // ]) - // }, [bits]) - - const saveBits = () => { - const newValue = {} - bits.forEach(({bit, description}) => (newValue[bit] = description)) - if(!onChange(newValue)) - setIsModalVisible(false) - } - - const addNewBit = (bit) => { - bit.key = Date.now() - setBits((prevBits) => [...(prevBits ?? []), bit]) - } - - const editBit = (bit) => { - if (!bit.key) return - const idx = bits.findIndex(v => v.key === bit.key) - if (idx < 0) return - setBits((prevBits) => { - prevBits[idx] = bit - return prevBits - }) - } - - const removeBit = (bit) => { - if (!bit.key) return - const idx = bits.findIndex(v => v.bit === bit.bit) - if (idx < 0) return - setBits((prevBits) => { - prevBits.splice(idx, 1) - return prevBits - }) - } - - return ( - <> - - setIsModalVisible(false)} - onOk={saveBits} - okText={'Сохранить'} - > - - - - ) -}) - -export default PermissionBits diff --git a/src/pages/AdminPanel/PermissionController.jsx b/src/pages/AdminPanel/PermissionController.jsx deleted file mode 100644 index 628c95a..0000000 --- a/src/pages/AdminPanel/PermissionController.jsx +++ /dev/null @@ -1,60 +0,0 @@ -import { useEffect, useState } from 'react' -import { invokeWebApiWrapperAsync } from '../../components/factory' -import LoaderPortal from '../../components/LoaderPortal' -import { EditableTable, makeActionHandler, makeColumn } from '../../components/Table' -import { AdminPermissionInfoService } from '../../services/api' -import PermissionBits, { toHexString } from './PermissionBits' - -export const PermissionController = () => { - const [permissions, setPermissions] = useState([]) - const [showLoader, setShowLoader] = useState(false) - - const columns = [ - makeColumn('Название', 'name', { width: 200, editable: true }), - makeColumn('Описание', 'description', { width: 200, editable: true }), - makeColumn('Значения битов', 'bitDescription', { - width: 200, - editable: true, - input: , // TODO: Дописать колонку для описания битов права - render: (bits) => { - if (!bits) return '--' - const sum = Object.keys(bits).reduce((sum, key) => sum + (1 << parseInt(key)), 0) - return sum && toHexString(sum, 16) - }, - }) - ] - - const updateTable = () => invokeWebApiWrapperAsync( - async () => { - const permissions = await AdminPermissionInfoService.getAll() - setPermissions(permissions) - }, - setShowLoader, - `Не удалось загрузить список прав` - ) - - useEffect(updateTable, []) - - const handlerProps = { - service: AdminPermissionInfoService, - setLoader: setShowLoader, - errorMsg: `Не удалось выполнить операцию`, - onComplete: updateTable - } - - return ( - - - - ) -} - -export default PermissionController diff --git a/src/pages/AdminPanel/RoleController.jsx b/src/pages/AdminPanel/RoleController.jsx new file mode 100644 index 0000000..43331c9 --- /dev/null +++ b/src/pages/AdminPanel/RoleController.jsx @@ -0,0 +1,141 @@ +import { Button, Modal } from 'antd' +import { useEffect, useState } from 'react' +import { AdminUserRoleService } from '../../services/api' +import LoaderPortal from '../../components/LoaderPortal' +import { invokeWebApiWrapperAsync } from '../../components/factory' +import { EditableTable, makeActionHandler, makeColumn, makeSelectColumn } from '../../components/Table' + +export const toHexString = (num, size) => '0x' + ('0'.repeat(size) + num.toString(16).toUpperCase()).slice(-size) + +const columns = [ + makeColumn('Имена прав', 'name', { + width: 400, + editable: true, + formItemRules: [{ + required: true, + message: 'Пожалуйста, введите имя права' + }], + }), +] + +export const RolePermissions = ({ value, onChange }) => { + const [isModalVisible, setIsModalVisible] = useState(false) + const [list, setList] = useState([]) + + const save = () => { + const newValue = list.map((value) => value.name) + if(!onChange(newValue)) + setIsModalVisible(false) + } + + const add = (permission) => { + permission.key = Date.now() + setList((prevList) => { + if (!prevList) prevList = [] + prevList.push(permission) + return prevList + }) + } + + const edit = (permission) => { + if (!permission.key) return + const idx = list.findIndex(v => v.key === permission.key) + if (idx < 0) return + setList((prevList) => { + prevList[idx] = permission + return prevList + }) + } + + const remove = (permission) => { + if (!permission.key) return + const idx = list.findIndex(v => v.key === permission.key) + if (idx < 0) return + setList((prevList) => { + prevList.splice(idx, 1) + return prevList + }) + } + + return ( + <> + + setIsModalVisible(false)} + onOk={save} + okText={'Сохранить'} + > + + + + ) +} + +export const RoleController = () => { + const [roles, setRoles] = useState([]) + const [showLoader, setShowLoader] = useState(false) + const [columns, setColumns] = useState([]) + + const updateTable = () => invokeWebApiWrapperAsync( + async () => { + const roles = await AdminUserRoleService.getAll() + setRoles(roles) + }, + setShowLoader, + `Не удалось загрузить список прав` + ) + + useEffect(() => { + const options = roles?.map((r) => ({ value: r.id, label: r.caption })) ?? [] + setColumns([ + makeColumn('Название', 'caption', { width: 200, editable: true }), + makeSelectColumn('Роль-родитель', 'idParent', options, options[0], { + width: 200, + editable: true + }), + makeColumn('Права доступа', 'permissions', { + width: 200, + editable: true, + input: , + render: (permissions) => permissions?.join(', ') ?? '', + }) + ]) + }, [roles]) + + useEffect(updateTable, []) + + const handlerProps = { + service: AdminUserRoleService, + setLoader: setShowLoader, + errorMsg: `Не удалось выполнить операцию`, + onComplete: updateTable + } + + return ( + + + + ) +} + +export default RoleController diff --git a/src/pages/AdminPanel/index.jsx b/src/pages/AdminPanel/index.jsx index 2ec84b4..f5d1e0b 100644 --- a/src/pages/AdminPanel/index.jsx +++ b/src/pages/AdminPanel/index.jsx @@ -2,14 +2,13 @@ import React, { Suspense } from 'react' import { Layout, Menu } from 'antd' import { Switch, Link, useParams, Redirect, Route } from 'react-router-dom' import { PrivateMenuItem, PrivateRoute } from '../../components/Private' -import { PermissionNames, PermissionValue } from '../../utils/permissions' const ClusterController = React.lazy(() => import('./ClusterController')) const CompanyController = React.lazy(() => import('./CompanyController')) const DepositController = React.lazy(() => import('./DepositController')) const UserController = React.lazy(() => import('./UserController')) const WellController = React.lazy(() => import('./WellController')) -const PermissionController = React.lazy(() => import('./PermissionController')) +const RoleController = React.lazy(() => import('./RoleController')) export const AdminPanel = () => { const { tab } = useParams() @@ -17,28 +16,24 @@ export const AdminPanel = () => { return ( - - + + Месторождения - + Кусты - + Скважины - + Пользователи - + Компании - - Права + + Права @@ -46,12 +41,12 @@ export const AdminPanel = () => { Loading...}> - - - - - - + + + + + + diff --git a/src/pages/Documents/menuItems.jsx b/src/pages/Documents/menuItems.jsx index 7abf3c7..976d34b 100644 --- a/src/pages/Documents/menuItems.jsx +++ b/src/pages/Documents/menuItems.jsx @@ -1,59 +1,50 @@ -//import { Menu } from "antd"; -import { FolderOutlined } from "@ant-design/icons"; -import { Link, Route} from "react-router-dom"; +import path from 'path' +import { FolderOutlined } from '@ant-design/icons' +import { Link, Route } from 'react-router-dom' +import { PrivateMenuItem } from '../../components/Private' +import { getUserRoles, isInRole } from "../../utils/PermissionService"; import DocumentsTemplate from './DocumentsTemplate' -import {PrivateMenuItem} from '../../components/Private' export const documentCategories = [ - {id:1, key:'fluidService', title:'Растворный сервис'/*, roles:['ff']*/}, - {id:2, key:'cementing', title:'Цементирование'}, - {id:3, key:'nnb', title:'ННБ'}, - {id:4, key:'gti', title:'ГТИ'}, - {id:5, key:'documentsForWell', title:'Документы по скважине'}, - {id:6, key:'supervisor', title:'Супервайзер'}, - {id:7, key:'master', title:'Мастер'}, + { id: 1, key: 'fluidService', title: 'Растворный сервис' }, + { id: 2, key: 'cementing', title: 'Цементирование' }, + { id: 3, key: 'nnb', title: 'ННБ' }, + { id: 4, key: 'gti', title: 'ГТИ' }, + { id: 5, key: 'documentsForWell', title: 'Документы по скважине' }, + { id: 6, key: 'supervisor', title: 'Супервайзер' }, + { id: 7, key: 'master', title: 'Мастер' }, ] const makeMenuItem = (keyValue, rootPath, title, other) => ( - + {title} - ) + +) const makeRouteItem = (keyValue, rootPath, other) => ( - + - ) + +) -const formatRoutePath = (rootPath) =>{ - let root = rootPath.endsWith('/') - ? rootPath.slice(0,-1) - : rootPath - - root = root.endsWith('/document') - ? root - : `${root}/document` - - return root -} - -const getCategoriesByUserRole = (role) => documentCategories - .filter(cat => !cat.roles || cat.roles.includes('*') || cat.roles.includes(role)) +const getCategoriesByUserRole = (role) => documentCategories.filter(cat => isInRole(cat.roles)) export const makeMenuItems = (rootPath) => { - const root = formatRoutePath(rootPath) - const categories = getCategoriesByUserRole(localStorage['roleName']) - return categories.map(category => + const root = path.join(rootPath, '/document') + const categories = getCategoriesByUserRole(getUserRoles()) + return categories.map(category => makeMenuItem(category.key, root, category.title, {icon:})) } -export const makeRouteItems = (rootPath, idWell) => { - const root = formatRoutePath(rootPath) - const categories = getCategoriesByUserRole(localStorage['roleName']) - const routes = categories.map(category => - makeRouteItem(category.key, root, {idCategory: category.id, idWell: idWell})) - return routes; -} \ No newline at end of file +export const makeRouteItems = (rootPath, idWell) => { + const root = path.join(rootPath, '/document') + const categories = getCategoriesByUserRole(getUserRoles()) + const routes = categories.map(category => makeRouteItem(category.key, root, { + idCategory: category.id, + idWell: idWell + })) + return routes +} diff --git a/src/utils/PermissionService.ts b/src/utils/PermissionService.ts new file mode 100644 index 0000000..cc2ba14 --- /dev/null +++ b/src/utils/PermissionService.ts @@ -0,0 +1,62 @@ +import { getArrayFromLocalStorage } from './storage' + +export type Role = string +export type Permission = string + +export const getUserRoles = (): Role[] => getArrayFromLocalStorage('roles') ?? [] +export const getUserPermissions = (): Permission[] => + getArrayFromLocalStorage('permissions') ?? [] + +export const hasPermission = (permission?: Permission): boolean => { + if (typeof permission !== 'string') return true + return permission in getUserPermissions() +} + +export const isInRole = (roles?: Role[] | Role): boolean => { + if (typeof roles === 'string' && !Array.isArray(roles)) + roles = [roles] + if (!roles?.length) return true + + if (localStorage.getItem('login') === 'dev') return true // TODO: Удалить строку + + const user_roles = getUserRoles() + return roles.some((role) => role in user_roles) +} + +/* + deposit + cluster + well (R) + well/archive (R) + well/message (R) + well/report (R) + well/measure (RW) + well/drillingProgram (RU согласовать) + well/telemetry (R) + well/telemetry:status (RW) + well/telemetry:rop (RW) + well/operations (R) + well/operations/tvd (R) + well/operations/sections (R) + well/operations/plan (RW) + well/operations/fact (RW) + well/operations/drillProccesFlow (RW) + well/operations/params (RW) + well/operations/composite (R) + well/operations/composite/wells (R) + well/operations/composite/sections (RW) + well/document (R) + well/document/fluidService (RU) + well/document/cementing (RU) + well/document/nnb (RU) + well/document/gti (RU) + well/document/documentsForWell (RU) + well/document/supervisor (RU) + well/document/master (RU) + admin (R) + admin/deposit (RAED) + admin/cluster (RAED) + admin/well (RAED) + admin/user (RAED) + admin/company (RAED) +*/ diff --git a/src/utils/permissions.ts b/src/utils/permissions.ts deleted file mode 100644 index 3e30d61..0000000 --- a/src/utils/permissions.ts +++ /dev/null @@ -1,140 +0,0 @@ -export type Role = string -export type PermissionName = string -export enum PermissionValue { - Nothing = 0, - Read = 1 << 0, - Write = 1 << 1, - Aprove = 1 << 1, - Update = 1 << 1, - Insert = 1 << 2, - Delete = 1 << 3, -} -export type PermissionMixingType = 'and' | 'or' -export type Permissions = [PermissionName, PermissionValue][] - -export const PermissionNames = { - deposit: 'deposit', - cluster: 'cluster', - well: { - _self: 'well', - archive: 'well/archive', - measure: 'well/measure', - message: 'well/message', - report: 'well/report', - drillingProgram: 'well/drillingProgram', - telemetry: { - _self: 'well/telemetry', - status: 'well/telemetry/status', - rop: 'well/telemetry/rop', - }, - operations: { - _self: 'well/operations', - tvd: 'well/operations/tvd', - sections: 'well/operations/sections', - plan: 'well/operations/plan', - fact: 'well/operations/fact', - drillProccesFlow: 'well/operations/drillProccesFlow', - params: 'well/operations/params', - composite: { - _self: 'well/operations/composite', - wells: 'well/operations/composite/wells', - sections: 'well/operations/composite/sections', - } - }, - document: { - _self: 'well/document', - fluidService: 'well/document/fluidService', - cementing: 'well/document/cementing', - nnb: 'well/document/nnb', - gti: 'well/document/gti', - documentsForWell: 'well/document/documentsForWell', - supervisor: 'well/document/supervisor', - master: 'well/document/master' - }, - }, - admin: { - _self: 'admin', - deposit: 'admin/deposit', - cluster: 'admin/cluster', - well: 'admin/well', - user: 'admin/user', - company: 'admin/company', - } -} - -const admins: Role[] = ['администратор', 'админ', 'admin'] - -export const hasPermissions = (permissions?: Permissions): boolean => { - if (!permissions?.length) - return true - return true // TODO: Написать проверку на доступ через права - return false -} - -export const isInRole = (roles?: Role[]): boolean => { - if (localStorage['login'] === 'dev') return true // TODO: Удалить строку - if (!roles?.length) - return true - // TODO: Переписать проверку на доступ через роли - const role: Role = localStorage['roleName']?.toLowerCase() - if (admins.indexOf(role) > -1) - return true - for (const r of roles) - if (r.toLowerCase() === role) - return true - return false -} - -export type hasAccessProps = { - permissions?: Permissions - roles?: Role[] - mixing?: PermissionMixingType -} - -export const hasAccess = ({permissions, roles, mixing}: hasAccessProps): boolean => { - switch (mixing) { - case 'or': - return hasPermissions(permissions) || isInRole(roles) - case 'and': - default: - return hasPermissions(permissions) && isInRole(roles) - } -} - -/* - deposit - cluster - well (R) - well/archive (R) - well/message (R) - well/report (R) - well/measure (RW) - well/drillingProgram (RU согласовать) - well/telemetry (R) - well/telemetry:status (RW) - well/telemetry:rop (RW) - well/operations (R) - well/operations/tvd (R) - well/operations/sections (R) - well/operations/plan (RW) - well/operations/fact (RW) - well/operations/drillProccesFlow (RW) - well/operations/params (RW) - well/operations/composite (R) - well/operations/composite/wells (R) - well/operations/composite/sections (RW) - well/document (R) - well/document/fluidService (RU) - well/document/cementing (RU) - well/document/nnb (RU) - well/document/gti (RU) - well/document/documentsForWell (RU) - well/document/supervisor (RU) - well/document/master (RU) - admin (R) - admin/deposit (RAED) - admin/cluster (RAED) - admin/well (RAED) - admin/user (RAED) - admin/company (RAED) -*/ diff --git a/src/utils/storage.ts b/src/utils/storage.ts new file mode 100644 index 0000000..a9c99d2 --- /dev/null +++ b/src/utils/storage.ts @@ -0,0 +1,5 @@ +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) +}