@fto-consult/expo-ui
Version:
Bibliothèque de composants UI Expo,react-native
492 lines (482 loc) • 22.2 kB
JavaScript
// Copyright 2022 @fto-consult/Boris Fouomene. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/****
* @see : https://swr.vercel.app/examples/infinite-loading
* @see :
*/
import Datagrid from "./IndexComponent";
import {defaultStr,defaultObj,defaultVal,isNonNullString,defaultNumber,isObjOrArray,isObj,extendObj} from "$cutils";
import {Pressable} from "react-native";
import React from "$react";
import Auth from "$cauth";
import DateLib from "$lib/date";
import {getFetchOptions} from "$cutils/filters";
import {setQueryParams} from "$cutils/uri";
import {uniqid} from "$cutils";
import apiFetch from "$capi/fetch";
import Icon from "$ecomponents/Icon";
import Label from "$ecomponents/Label";
import { StyleSheet} from "react-native";
import View from "$ecomponents/View";
import theme from "$theme";
import {getRowsPerPagesLimits} from "./Common/utils";
import PropTypes from "prop-types";
import {Menu} from "$ecomponents/BottomSheet";
import session from "$session";
import {useScreen,useSWR} from "$econtext/hooks";
import {HStack} from "$ecomponents/Stack";
export const getSessionKey = ()=>{
return Auth.getSessionKey("swrDatagrid");
}
export const getSessionData = (key)=>{
const data = defaultObj(session.get(getSessionKey()));
return isNonNullString(key) ? data[key.trim()] : data;
}
export const setSessionData = (key,value)=>{
const d = getSessionData();
if(isObj(key)){
return session.set(getSessionKey(),extendObj({},d,key));
}
d[key] = value;
return session.set(getSessionKey(),d);
}
const isValidMakePhoneCallProps = p=> isObj(p) && Object.size(p,true) || typeof p ==='function';
/****la fonction fetcher doit toujours retourner :
* 1. la liste des éléments fetchés dans la props data
* 2. le nombre total d'éléments de la liste obtenue en escluant les clause limit et offset correspondant à la même requête
*/
const SWRDatagridComponent = React.forwardRef((props,ref)=>{
let {
table,
data:customData,
saveButton,
title:customTitle,
fab,
rowKey,
actions,
sessionName,
server,
columns,
canMakePhoneCall,
makePhoneCallProps,
fetchData,
fetchPath,
fetchPathKey,
fetcher,
ListFooterComponent,
testID,
autoSort,
fetchOptions:customFetchOptions,
handleQueryLimit,
handlePagination,
onFetchData,
beforeFetchData,
sort,
defaultSortColumn,
defaultSortOrder,
isLoading : customIsLoading,
icon : cIcon,
swrOptions,
pagination,
renderCustomPagination,
...rest
} = props;
const screenContext = useScreen();
rest = defaultObj(rest);
pagination = defaultObj(pagination);
rest.exportTableProps = defaultObj(rest.exportTableProps)
const firstPage = 1;
const tableName = defaultStr(table?.tableName,table?.table,rest?.tableName,rest?.table).trim().toUpperCase();
defaultSortColumn = defaultStr(defaultSortColumn,table?.defaultSortColumn);
defaultSortOrder = defaultStr(defaultSortOrder,table?.defaultSortOrder).toLowerCase().trim();
sort = isNonNullString(sort)? {column:sort} : isObj(sort)?sort : {};
const sColumn = defaultStr(sort.column,defaultSortColumn);
if(sColumn){
sort.column = sColumn;
sort.dir = defaultStr(sort.dir).toLowerCase().trim();
if(!['asc','desc'].includes(sort.dir) && ['asc','desc'].includes(defaultSortOrder)){
sort.dir = defaultSortOrder;
}
} else {
delete sort.column;
}
canMakePhoneCall = defaultBool(canMakePhoneCall,table?.canMakePhoneCall);
makePhoneCallProps = isValidMakePhoneCallProps(makePhoneCallProps) && makePhoneCallProps || isValidMakePhoneCallProps(rest.makePhoneCallProps) && rest.makePhoneCallProps || isValidMakePhoneCallProps(table?.makePhoneCallProps) && table?.makePhoneCallProps || {};
rowKey = defaultStr(rowKey,table?.rowKey,table?.primaryKeyColumnName);
const title = React.isValidElement(customTitle,true) && customTitle || defaultStr(table?.label,table?.text)
columns = (isObj(columns) || Array.isArray(columns)) && Object.size(columns,true) && columns || table?.fields;
const fetchFields = [];
Object.map(columns,(column,i)=>{
if(isObj(column)){
fetchFields.push(defaultStr(column.field,i));
}
});
actions = defaultVal(table?.actions,actions);
if(isObj(table) && isObj(table?.datagrid)){
for(let i in Datagrid.propTypes){
if(i in table){
rest[i] = isObj(rest[i])? extendObj({},rest[i],table[i]) : table[i];
}
}
}
rest.actions = actions;
rest.columns = columns || [];
const icon = defaultStr(cIcon,table?.icon);
rest.tableName = tableName;
rest.canMakePhoneCall = canMakePhoneCall;
rest.makePhoneCallProps = makePhoneCallProps;
rest.exportTableProps.fileName = defaultStr(rest.exportTableProps.fileName,title+"-"+DateLib.format(DateLib.toObj(),'dd-mm-yyyy HH-MM'))
rest.exportTableProps.pdf = defaultObj(rest.exportTableProps.pdf);
rest.exportTableProps.pdf = extendObj(true,{},{
fileName : rest.exportTableProps.fileName,
title : React.getTextContent(title),
},rest.exportTableProps.pdf);
const fetchOptionsRef = React.useRef({});
const isFetchPathNull = fetchPath === null || fetchPath ===false;
const fPathRef = React.useRef(defaultStr(fetchPathKey,"defaultFetchPathKey"));
fetchPath = defaultStr(fetchPath,table?.queryPath,tableName.toLowerCase()).trim();
if(fetchPath){
fetchPath = setQueryParams(fetchPath,"SWRFetchPathKey",fPathRef.current)
}
const sortRef = React.useRef({});
const innerRef = React.useRef(null);
const showProgressRef = React.useRef(true);
const forceRefreshRef = React.useRef(true);
const pageRef = React.useRef(defaultNumber(pagination.start,1));
const canHandlePagination = handlePagination !== false ? true : false;
const canHandleLimit = handleQueryLimit !== false && canHandlePagination ? true : false;
const limitRef = React.useRef(!canHandleLimit ?0 : defaultNumber(getSessionData("limit"),pagination.limit,500));
const isInitializedRef = React.useRef(false);
const hasFetchedRef = React.useRef(false);
swrOptions = defaultObj(swrOptions);
swrOptions.revalidateOnMount = typeof swrOptions.revalidateOnMount =="boolean"? swrOptions.revalidateOnMount : false;
testID = defaultStr(testID,"RNSWRDatagridComponent");
const {error, isValidating,isLoading,data:result,refresh} = useSWR(isFetchPathNull?null:fetchPath,{
fetcher : (url,opts)=>{
if(!isInitializedRef.current) {
return Promise.resolve({data:[],total:0});
}
opts = defaultObj(opts);
opts.fetchOptions = isObj(opts.fetchOptions)? Object.clone(opts.fetchOptions) : {};
extendObj(true,opts.fetchOptions,fetchOptionsRef.current);
if(props.parseMangoQueries === false){
opts.fetchOptions.selector = extendObj(true,{},opts.fetchOptions.selector,fetchOptionsRef.current?.selector);
}
opts.fetchOptions.sort = sortRef.current;
if(canHandleLimit && limitRef.current > 0){
opts.fetchOptions.limit = limitRef.current;
opts.fetchOptions.page = pageRef.current -1;
} else {
delete opts.limit;
delete opts.fetchOptions.limit;
delete opts.fetchOptions.page;
delete opts.page;
delete opts.offset;
}
opts.url = opts.path = url;
if(showProgressRef.current ===false || typeof forceRefreshRef.current !=='boolean'){
opts.showError = false;
}
const end = (a)=> {
hasFetchedRef.current = true;
return a;
};
if(typeof fetcher =='function'){
return Promise.resolve(fetcher(url,opts)).then(end);
}
return apiFetch(url,opts).then(end);
},
swrOptions,
});
const dataRef = React.useRef(null);
const totalRef = React.useRef(0);
const prevIsLoading = React.usePrevious(isLoading);
const loading = (customIsLoading === true || isLoading || (isValidating && showProgressRef.current));
const {data,total} = React.useMemo(()=>{
if((loading && customIsLoading !== false) || !isObjOrArray(result)){
return {data:dataRef.current,total:totalRef.current};
}
let {data,total} = (Array.isArray(result) ? {data:result,total:result.length} : isObj(result)? result : {data:[],total:0});
const dd = Object.size(data);
if(typeof total !=='number'){
total = dd;
} else if(dd>total){
total = dd;
}
if(onFetchData && typeof onFetchData =='function'){
onFetchData({allData:data,total,data,context:innerRef.current})
}
dataRef.current = data;
totalRef.current = total;
return {data,total};
},[result,loading])
React.useEffect(()=>{
setTimeout(x=>{
if(error && innerRef.current && innerRef.current.isLoading && innerRef.current.isLoading()){
innerRef.current.setIsLoading(false,false);
}
},500);
},[error]);
const doRefresh = (showProgress)=>{
showProgressRef.current = showProgress || typeof showProgress ==='boolean' ? showProgress : false;
const fPath = isNonNullString(fetchPath)? fetchPath : fPathRef.current;
const rKey = `${setQueryParams(fPath,"swrRefreshKeyId",uniqid("swr-refresh-key"))}`;
forceRefreshRef.current = true;
refresh(rKey);
}
const canPaginate = ()=>{
if(!canHandlePagination) return false;
if(canHandleLimit && typeof total !=='number' || typeof pageRef.current !='number' || typeof limitRef.current !='number') return false;
if(limitRef.current <= 0) return false;
return true;
}
const getTotalPages = ()=>{
if(!canPaginate()) return false;
return Math.ceil(total / limitRef.current);;
};
const getNextPage = ()=>{
if(!canPaginate()) return false;
const totalPages = getTotalPages();
let nPage = pageRef.current+1;
if(nPage > totalPages){
nPage = totalPages;
}
if(nPage === pageRef.current){
return false;
}
return nPage;
},getPrevPage = ()=>{
if(!canPaginate()) return false;
let pPage = pageRef.current - 1;
if(pPage < firstPage){
pPage = firstPage;
}
if(pPage === pageRef.current){
return false;
}
return pPage;
}, canSortRemotely = ()=>{
if(!canPaginate() || autoSort === true) return false;
///si le nombre total d'élements est inférieur au nombre limite alors le trie peut être fait localement
return total > limitRef.current && true || false;
}
const pointerEvents = loading ?"node" : "auto";
const itLimits = [{
text : "Limite nbre elts par page",
divider : true,
}]
getRowsPerPagesLimits().map((item)=>{
itLimits.push({
text : item.formatNumber(),
icon : limitRef.current == item ? 'check' : null,
primary : limitRef.current === item ? true : false,
tooltip : item === 0 ? "Sélectionnez cette valeur si vous souhaitez vous en passer de la limite du nombre d'items à afficher" : item.formatNumber(),
onPress : ()=>{
if(item == limitRef.current) return;
limitRef.current = item;
setSessionData("limit",limitRef.current);
pageRef.current = firstPage;
setTimeout(() => {
doRefresh(true);
}, (500));
}
});
});
React.useEffect(()=>{
if(hasFetchedRef.current){
showProgressRef.current = false;
}
},[showProgressRef.current]);
const isAppLoading = loading && showProgressRef.current && forceRefreshRef.current || false;
forceRefreshRef.current = undefined;
return (
<Datagrid
testID = {testID}
accordionProps = {table?.accordionProps}
{...defaultObj(table?.datagrid)}
{...rest}
renderProgressBar = {screenContext && screenContext?.isFocused() === false?false : rest.renderProgressBar || table?.datagrid?.renderProgressBar}
fetchOptions = {customFetchOptions}
title = {customTitle || title || undefined}
sort = {sort}
onSort = {({sort})=>{
sortRef.current = sort;
if(!canSortRemotely()) return;
pageRef.current = firstPage;
doRefresh(true);
return false;
}}
renderCustomPagination = {(...args)=>{
const cPagination = typeof renderCustomPagination =="function"? renderCustomPagination(...args) : null;
if(!canPaginate()) {
return <HStack testID={testID+"_PaginationLabel"}>
{React.isValidElement(cPagination)? cPagination : null}
<Label textBold primary style={{fontSize:15}}>
{total.formatNumber()}
</Label>
</HStack>
}
const page = pageRef.current, totalPages = getTotalPages(), prevPage = getPrevPage(),nextPage = getNextPage();
const iconProp = {
size : 25,
style : [theme.styles.noMargin,theme.styles.noPadding],
}
const sStyle = [styles.limitStyle1,theme.styles.noPadding,theme.styles.noMargin];
return <View testID={testID+"_PaginationContainer"} pointerEvents={pointerEvents}>
<View style={[theme.styles.row,theme.styles.w100]} pointerEvents={pointerEvents} testID={testID+"_PaginationContentContainer"}>
{React.isValidElement(cPagination)? cPagination : null}
<Menu
testID={testID+"_SimpleSelect"}
style = {sStyle}
anchor = {(p)=>{
return <Pressable style={[theme.styles.noMargin,theme.styles.noPadding]} {...p} testID={testID+"MenuSelectLimit"}>
<Label primary testID={testID+"_Label"} fontSize={16}>
{limitRef.current.formatNumber()}
</Label>
</Pressable>
}}
title = {'Limite du nombre d\'éléments par page'}
items = {itLimits}
/>
<Icon
///firstPage
{...iconProp}
title = {"Aller à la première page"}
name="material-first-page"
disabled = {pageRef.current <= 1 && true || false}
onPress = {()=>{
if(pageRef.current <= firstPage) return;
pageRef.current = firstPage;
doRefresh(true);
}}
/>
<Icon
//decrement
{...iconProp}
title = {"Aller à la page précédente {0}".sprintf(prevPage && prevPage.formatNumber()||undefined)}
name="material-keyboard-arrow-left"
disabled = {page === prevPage || getPrevPage() === false ? true : false}
onPress = {()=>{
const page = getPrevPage();
if(page === false) return;
if(pageRef.current === page) return;
pageRef.current = page;
doRefresh(true);
}}
/>
<View testID={testID+"_PaginationLabel"}>
<Label style={{fontSize:15}}>
{(total?page:0).formatNumber()}-{totalPages.formatNumber()}{" / "}{total.formatNumber()}
</Label>
</View>
<Icon
//increment
{...iconProp}
title = {"Aller à la page suivante {0}".sprintf(nextPage && nextPage.formatNumber()||undefined)}
name="material-keyboard-arrow-right"
disabled = {nextPage > totalPages || getNextPage() === false ? true : false}
onPress = {()=>{
const page = getNextPage();
if(page === false) return;
if(pageRef.current === page) return;
pageRef.current = page;
doRefresh(true);
}}
/>
<Icon
//lastPage
{...iconProp}
name="material-last-page"
title = {"Aller à la dernière page {0}".sprintf(totalPages && totalPages.formatNumber()||undefined)}
disabled = {page >= totalPages ? true : false}
onPress = {()=>{
const page = getTotalPages();
if(pageRef.current >= page) return;
pageRef.current = page;
doRefresh(true);
}}
/>
</View>
</View>
}}
handleQueryLimit = {false}
handlePagination = {false}
autoSort = {canSortRemotely()? false : true}
isLoading = {isAppLoading}
beforeFetchData = {(args)=>{
let {fetchOptions:opts,force,renderProgressBar} = args;
opts = getFetchOptions({showError:showProgressRef.current,...opts});
fetchOptionsRef.current = opts.fetchOptions;
opts.fetchOptions.withTotal = true;
sortRef.current = opts.fetchOptions.sort;
isInitializedRef.current = true;
if(force){
pageRef.current = firstPage;
}
if(typeof beforeFetchData =="function" && beforeFetchData(args)==false) return;
doRefresh(typeof renderProgressBar =='boolean'? renderProgressBar : showProgressRef.current);
return false;
}}
isSWRDatagrid
isTableData
fetchData = {undefined}
data = {data}
canMakePhoneCall={canMakePhoneCall}
key={tableName}
sessionName={defaultStr(sessionName,'list-data')}
ref={React.useMergeRefs(ref,innerRef)}
rowKey={rowKey}
renderEmpty = {(p)=>{
return <View style={styles.emptyAccordion}>
{icon ? <Icon name={icon} color={theme.colors.primaryOnSurface} size={80}/> : null}
{<Label secondary style={styles.labelTitle}>{title}</Label>}
<Label style={[styles.emptyText]}>
{"Aucune données enrégistrée!!"}
</Label>
</View>
}}
/>
)
});
export default SWRDatagridComponent;
SWRDatagridComponent.displayName = "SWRDatagridComponent";
SWRDatagridComponent.propTypes = {
...Datagrid.propTypes,
swrOptions : PropTypes.object,//les ooptions supplémentaires à passer à la fonction swr
handlePagination : PropTypes.bool, //spécifie si le datagrid prendra en compte la pagination
/*** le nom de la colonne de trie par défaut */
defaultSortColumn : PropTypes.string,
fetchPath : PropTypes.oneOfType([PropTypes.string,PropTypes.bool,PropTypes.object]),
fetchPathKey : PropTypes.string,//la clé permettant de suffixer l'url fecherPath afin que ce ne soit pas unique pour certaines tables
fetchData : PropTypes.func,
table : PropTypes.shape({
tableName : PropTypes.string,
table : PropTypes.string,
fields : PropTypes.oneOfType([
PropTypes.objectOf(PropTypes.object),
PropTypes.arrayOf(PropTypes.object),
])
}),
}
const styles = StyleSheet.create({
emptyAccordion : {
alignSelf : 'center',
alignItems:'center'
},
labelTitle: {
fontSize : 18,
},
limitStyle : {
backgroundColor:'transparent',
width:50,
height : 35,
},
emptyText : {
fontSize : 16,
fontWeight : 'bold',
flexWrap : 'wrap',
marginVertical : 10,
textAlign : 'center'
}
})