UNPKG

next-gs

Version:

NPM package for building a React+NextJS+Prisma admin application.

379 lines (303 loc) 8.14 kB
import React from "react"; import { useSelector, useDispatch } from "react-redux"; import { resReducer, updateData, updateDataItem, type ResState, } from "@next-gs/utils/store/res"; import fn from "@next-gs/utils/funcs"; import notify from "@next-gs/utils/notify"; import { AdminError, type RepoPersistParams, type RepoActionResult, type RepoDestroyResult, type Entity, type EntityID, type Resource, type ResourceRef, type ResourceService, type ResourceState, type ResServicePager, type ResServiceRefreshParams, type ResServiceRefreshResult, } from "@next-gs/client"; import { useAdmin } from "./useAdmin"; import { useAuth } from "./useAuth"; type ResStoreSelector<E extends Entity> = ( callback: (state: { res: ResState }) => ResourceState<E>, ) => ResourceState<E>; // biome-ignore lint/suspicious/noExplicitAny: <explanation> type ResStoreDispatch = React.ActionDispatch<any>; type ResStore<E extends Entity> = { resource: Resource<E>; state: ResourceState<E>; mutators: { updateData: (data: Partial<ResourceState<E>>) => void; updateDataItem: (item: Partial<E>, free?: boolean, index?: number) => void; }; }; function checkResponse(resp: RepoActionResult) { if (resp.error) throw new AdminError(resp.error); } const createResStore = <E extends Entity>( ref: ResourceRef<E>, selector: ResStoreSelector<E>, dispatch: ResStoreDispatch, ) => { const { getResource } = useAdmin(); const resource = getResource(ref); if (!resource) { throw new Error(`Resource reference "${ref}" not found`) } const state = selector(({ res }) => { const { data = [], keys = [], recs = {}, total = 0, query = {}, marks = [], pending = false, error, tree = { node: {}, open: {} }, item = null, } = fn.get(res, [resource.name], {}) as ResourceState<E>; const { where = resource.query?.where, sort = resource.query?.sort, skip = 0, take = 20, } = query; const { node = {}, open = {} } = tree; /*auth.getPref("list_pgsize", 10) if (page.current > 0) { if (fn.isNoU(page.size)) { page.size = ; } if (page.size > state.list.page.size) { page.current = 1; } }*/ return { data, keys, recs, total, pending, error, tree: { node, open }, item, query: { where, sort, skip, take, }, marks, } satisfies ResourceState<E>; }); return { resource, state, mutators: { updateData: (data: Partial<ResourceState<E>>) => dispatch( updateData({ res: resource.name, data: data as Partial<ResourceState<Entity>>, }), ), updateDataItem: (item: Partial<E>, free?: boolean, index?: number) => dispatch( updateDataItem({ res: resource.name, data: { item, free, index } }), ), }, } as ResStore<E>; }; export const useResStore = <E extends Entity>(store: ResStore<E>) => { const { resource, state, mutators } = store; const res = resource.name; const { actions: admin } = useAdmin(); const auth = useAuth(); const checkAction = (action: string) => { const allows = auth.hasRole(resource.role || `TBL${res}`, action); if (!allows) notify.error("Você não possui permissão para executar esta ação!"); return allows; }; const handleError = (error: Error | string) => { notify.error(error); return Promise.reject(error); }; const refresh = async (params: ResServiceRefreshParams<E>) => { const { nodeKey, ...query } = { ...state.query, ...params }; if (fn.notNil(query.take)) { auth.setPref("list_pgsize", query.take); } mutators.updateData({ query: query, error: null, pending: true, }); const { nodeField } = resource; if (nodeField && fn.notNil(nodeKey)) { fn.set(query, ["where", nodeField], nodeKey); } /* fn.clean({ ...list.filter, ...fetchFilter }); const { listFilter } = state.meta; if (listFilter) filter = fn.isFunction(listFilter) ? listFilter(filter) : fn.assign(filter, listFilter);*/ return await admin .getList<E, E>(res, query) .then((resp) => { checkResponse(resp); const data = { data: resp.data, keys: fn.map(resp.data, "id") as string[], recs: fn.keyBy(resp.data, "id"), total: resp.total, tree: { node: {}, open: {} }, } as ResServiceRefreshResult<E>; /*if (nodeKey) { node = { ...state.tree.node, [nodeKey]: data.ids }; open = { ...state.tree.open, [nodeKey]: data.ids.length > 0 }; } else { const root = []; fn.forEach(data.recs, (rec) => { const k = rec[nodeField]; if (fn.isNoU(k) || fn.isNil(recs[k])) { if (!root.includes(k)) root.push(k); } if (node[k] === undefined) { node[k] = []; open[k] = true; } node[k].push(rec.id); }); node["root"] = root; }*/ mutators.updateData({ ...data, error: null, pending: false, }); return data; }) .catch((error) => { mutators.updateData({ error, pending: false }); return handleError(error); }); }; const replace = <I extends Partial<E> | undefined>( item: I | I[], free?: boolean, index?: number, ) => { if (!item) return; const updater = (obj: I) => { if (!obj) return; const { id } = obj; const previous = id ? (state.recs[id] as E) : undefined; id && mutators.updateDataItem(obj, free, index); return previous; }; return fn.isArray(item) ? fn.map(item, updater) : updater(item); }; const refreshItem = (id: EntityID) => admin .getList<E, E>(res, { where: { id } }) .then(({ data }) => replace(data[0])) .catch(handleError); const select = async (entity?: E) => { mutators.updateData({ item: entity }); if (fn.notNil(entity?.id)) { mutators.updateData({ pending: true }); return await admin .getOne<E>(res, entity.id) .then(({ object }) => { const item = resource.onSelect?.(object) || object; mutators.updateData({ item, pending: false, error: undefined }); return item; }) .catch((error) => { mutators.updateData({ item: undefined, pending: false, error }); return handleError(error); }); } }; const persist = async (value: RepoPersistParams<E>) => { const previous = replace(value); try { const resp = await admin.persist<E>(res, value); checkResponse(resp); if (resp.object) { replace(resp.object); notify.info(`${resource.title.simple} salvo(a) com sucesso!`); return resp; } } catch (error) { if (previous) replace(previous); handleError(error as Error); } }; const destroy = async (ids: EntityID[]) => { if (!checkAction("D")) { throw new Error("Usuário não Autorizado!"); } const { handleDestroy } = resource; const destroyOne: (id: EntityID) => Promise<RepoDestroyResult<E>> = async ( id, ) => { if (handleDestroy) { return await handleDestroy(id, admin); } const index = fn.indexOf(state.keys, id); const previous = replace({ id } as E, true); return await admin.destroy<E>(res, id).catch((error) => { replace(previous, false, index); return Promise.reject(error); }); }; return await Promise.all( ids.map((id) => destroyOne(id).catch(handleError)), ); }; const navigate = ({ page = 1, pageSize = 20 }: Partial<ResServicePager>) => refresh({ skip: page * pageSize, take: pageSize }); const { total, query: { skip = 0, take = 20 }, } = state; return { res: resource, ...state, pager: { page: take > 0 ? Math.floor(skip / take) + 1 : 1, pageCount: Math.ceil(total / take), pageSize: take, }, select, navigate, refresh, persist, destroy, } as ResourceService<E>; }; export const useStoreResource = <E extends Entity>(ref: ResourceRef<E>) => { const dispatch = useDispatch(); const store = createResStore(ref, useSelector, dispatch); return useResStore(store); }; export const useSimpleResource = <E extends Entity>(ref: ResourceRef<E>) => { const [state, dispatch] = React.useReducer(resReducer, {}); const store = createResStore( ref, (callback) => callback({ res: state }), dispatch, ); return useResStore(store); };