@fto-consult/expo-ui
Version:
Bibliothèque de composants UI Expo,react-native
734 lines (721 loc) • 28.8 kB
JavaScript
import DateLib from "$lib/date";
import {isNonNullString,defaultStr,isNullOrEmpty,debounce,isFunction,uniqid} from "$cutils";
import {regexParser,regexActions,operators as _operators,actions as _actions,periodActions,betweenActions, inActions as _inActions,getFilterStateValues,getSessionData,setSessionData,filterDescription} from "$cfilters";
import {parseDecimal} from "$ecomponents/TextField";
import notify from "$notify";
import PropTypes from "prop-types";
import {extendObj} from "$cutils";
import {getFilterComponentProps} from "$ecomponents/Form/FormData/componentsTypes"
import Menu from "$ecomponents/BottomSheet/Menu";
import {StyleSheet,View} from "react-native";
import Icon from "$ecomponents/Icon";
import React,{Component as AppComponent} from "$react";
import theme from "$theme";
import {isMobileMedia} from "$cplatform/dimensions";
import { ActivityIndicator } from "react-native-paper";
import DialogProvider from "$ecomponents/Form/FormData/DialogProvider";
import FilterBetweenComponent from "./BetweenComponent";
export const dateTypes = ["date","time","datetime","date2time"]
const manualRunKey = "manual-run";
export * from "$cfilters";
/***** Coposant Filter, pour les filtres de données */
export default class Filter extends AppComponent {
constructor(props) {
super(props);
this.clearText = this.clearText.bind(this);
this.fireValueChanged = this.fireValueChanged.bind(this);
const {Component,props:filterProps,type} = getFilterComponentProps(props);
Object.defineProperties(this,{
Component : {
value : Component,
override:false,writable:false
},
filterProps : {value:filterProps},
type : {
value : type, override : false, writable : false
},
searchFilter : {value : React.createRef(null),},
name : {value : defaultStr(this.props.name,uniqid("no-name-filter"))}
})
this.filterValidationTimeout = 0;
switch(this.type){
case 'select' :
break;
case 'date':
break;
case 'time':
break;
case 'switch' :
break;
case 'checkbox':
break;
default :
this.filterValidationTimeout = defaultNumber(this.props.timeout,1500);
break;
}
Object.defineProperties(this,{
fireFilterSearch : {
value : this.filterValidationTimeout ? debounce(this.fireValueChanged.bind(this),this.filterValidationTimeout) : this.fireValueChanged.bind(this),
},
isInitializedRef : {
value : {current:false}
},
previousRef : {
value : {current : null}
},
manualRunRef : {value : {current : 0}},
})
extendObj(this.state,{
...this.initFiltersOp(),
manualRun : this.getSessionManualRunValue(),
});
this.autobind();
}
componentWillUnmount(){
super.componentWillUnmount();
}
toggleIgnoreCase(){
this.setState({ignoreCase:!this.state.ignoreCase},()=>{
if(!this.willRunManually()){
this.fireValueChanged();
};
})
}
setSessionManualRunValue(value){
this.manualRunRef.current = value;
}
getSessionManualRunValue(){
if(false && this.manualRunRef.current === undefined && this.isFilterSelect()){
this.manualRunRef.current = true;
}
return this.manualRunRef.current;
return getSessionData(manualRunKey) ? true : false
}
willRunManually (){
return this.state.manualRun;
}
toggleManualRun(){
this.setState({manualRun:!this.state.manualRun},()=>{
this.setSessionManualRunValue(this.state.manualRun?0:1);
if(!this.willRunManually()){
this.fireValueChanged(true);
}
})
}
getStateValues(){
return getFilterStateValues(this.state);
}
callOnValidate(arg){
arg = isObj(arg)? arg : {};
if(typeof this.props.onValidate ==='function'){
this.props.onValidate({...this.getStateValues(),field:this.props.name,...arg})
}
}
isDecimal(){
const t = defaultStr(this.type,this.props.type).toLowerCase();
return t =="number" || t =='decimal' ? true : false;
}
compareValues(v1,v2,...args){
return compareValues(this.prepareValue(v1),this.prepareValue(v2),...args);
}
onFilterValidate(arg){
arg = defaultObj(arg);
if(this.compareValues(this.state.defaultValue, arg.value)){
return;
}
this.setState({defaultValue:arg.value},()=>{
this.callOnValidate(arg);
if(!isNumber(this.filterValidationTimeout)){
this.filterValidationTimeout = 0;
}
this.fireFilterSearch(false);
});
}
getDefaultAction(type){
type = defaultStr(type,this.type,this.props.type).toLowerCase();
if(type.contains('select')){
return "$in";
}
if(dateTypes.includes(type)) return "$eq";
if(type !== 'number' && type !== 'decimal'){
return '$regexcontains';
}
return '$eq';
}
initFiltersOp(type){
type = defaultStr(type,this.type).toLowerCase();
let operators = {..._operators};
let actions = {...betweenActions,..._actions};
if(this.props.orOperator === false){
delete operators.$or;
}
if(this.props.andOperator === false){
delete operators.$and;
}
let action = this.props.action;
let operator = this.props.operator;
let ignoreCase = this.props.ignoreCase;
ignoreCase = defaultVal(ignoreCase,true);
let isTextFilter = false;
if(type =="checkbox" || type == 'switch'){
action = '$eq';
} else if(type.contains('select')){
actions = _inActions;
} else if(dateTypes.includes(type)) {
actions = {...periodActions, ...actions}
delete actions.$between;
} else if(type !== 'number' && type !== 'decimal'){
actions = {...betweenActions,...regexActions};
isTextFilter = true;
}
if(!action){
action = this.getDefaultAction(type);
}
let defaultValue = defaultVal(this.props.defaultValue);
operator = defaultVal(operator,"$and");
if(actions == _inActions || type.contains("select")){
defaultValue = isNonNullString(defaultValue)? defaultValue.split(",") : Array.isArray(defaultValue)? defaultValue : !isNullOrEmpty(defaultValue)? [defaultValue] : {};
}
return {actions,action,ignoreCase,operator,operators,manualRun:defaultBool(this.props.manualRun,false),defaultValue,isTextFilter};
}
/**** prepare la valeur afin qu'elle soit soumise au composant qui implémente le filtre en question */
prepareValue(value){
if(!isObjOrArray(value) && (isNullOrEmpty(value,true) || String(value).trim() ==='undefined') ){
value = undefined;
}
if(this.isDecimal()){
value = parseDecimal(value)
if(value == 0){
value = undefined;
}
}
return value;
}
isFilterSelect(){
return defaultStr(this.type,this.props.type).toLowerCase().includes("select");
}
fireValueChanged (forceRun){
if(this.willRunManually() && !forceRun) return;
let {defaultValue:value,action,ignoreCase,operator} = this.state;
let force = forceRun ===true ? true : false;
value = this.prepareValue(value);
if(action =="$today" || action =='$yesterday'){
force = true;
}
const prev = JSON.stringify(this.previousRef.current), current = {value,operator,action,ignoreCase};
if(prev == JSON.stringify(current) && (force !== true)){
return this;
}
this.previousRef.current = current;
if(isFunction(this.props.onChange)){
let selector = {};
selector[this.props.field] = action;
let originAction = action;
if(isNonNullString(action)){
action = action.toLowerCase().trim();
if(action =="$today" || action =='$yesterday'){
action = "$eq";
} else if(action.startsWith("$") && (action.contains("week") || action.contains("month"))){
action = "$period";
}
if(action.startsWith("$regex")){
let f = regexParser[action.ltrim("$regex")];
if(isFunction(f)){
value = f(value);
}
if(isNonNullString(value) && this.state.ignoreCase){
try {
const v = RegExp(value.ltrim("/").rtrim("/"),'i');
value = v;
} catch{}
}
action = "$regex";
}
if(operator == "$nin"){
if(isArray(value)){
value.push("");
}
}
}
this.props.onChange({...this.getStateValues(),value,field:this.props.field,columnField:this.props.field,action,operator,selector,originAction,context:this});
}
}
componentDidUpdate (){
super.componentDidUpdate();
this.canBindEvent = true;
}
componentDidMount(){
super.componentDidMount();
}
setIgnoreCase(ignoreCase){
if(!(this.searchFilter.current) ) return;
if(ignoreCase === this.state.ignoreCase) return;
this.setState({ignoreCase},()=>{
this.fireValueChanged();
});
}
runAction ({value,action}){
this.setState({action,defaultValue:value},()=>{
this.callOnValidate();
if(isFunction(this.searchFilter.current.getValue) && this.canBindEvent){
this.fireValueChanged();
}
})
}
isDateTime(){
const t = defaultStr(this.type,this.props.type);
return t.contains("date") && t.contains("time");
}
showBetweenSelector(args){
return this.showBetweenActionSelector(args);
}
showPeriodSelector(args){
return this.showBetweenActionSelector(args);
}
showBetweenActionSelector(args){
if(typeof args =='function'){
args = {success:args};
}
let {success,callback} = defaultObj(args);
const _type = defaultStr(this.type,this.props.type).toLowerCase().trim();
success = typeof success =='function'? success : typeof callback =='function'? callback : undefined;
const defaultValue = defaultStr(this.state.defaultValue).trim();
let split = defaultValue.split("=>");
const isDateTime = this.isDateTime();
let start = split[0] && split[0] || undefined, end = split[1] && split[1] || undefined;
const willHandleDate = _type.contains('date');
if(willHandleDate){
start = isDateTime ? new Date().toSQLDateTimeFormat() : new Date().toSQLDateFormat();
end = start;
if(DateLib.isValidSQLDateTime(split[0]) || DateLib.isValidSQLDate(split[0])){
start = split[0];
}
if(DateLib.isValidSQLDateTime(split[1]) || DateLib.isValidSQLDate(split[1])){
end = split[1];
}
}
const type = willHandleDate ? (isDateTime? "datetime" : "date"):_type;
const format = this.props.format;
return new Promise((resolve,reject)=>{
DialogProvider.open({
subtitle : false,
fields : {
start : {format,type,text:(willHandleDate?'Du':"Valeur inférieure"),defaultValue:start},
end : {format,type,text:willHandleDate?'Au':"Valeur supérieure",defaultValue:end}
},
title :"Définir {0} [{1}]".sprintf(willHandleDate?"une période":"Un intervalle",defaultStr(this.props.label,this.props.text)),
cancelButton : true,
actions : {
yes : {
text : 'Définir',
icon : "check"
},
},
onSuccess : ({data})=>{
const compare = (a,b)=>{
if(isNonNullString(a) && isNonNullString(b)){
return a.localeCompare(b) > 0;
}
return a > b;
}
if(data.start !== undefined && data.start !== null && data.end !== undefined && data.end !== null && compare(data.start,data.end)){
return notify.error("La {0} doit être supérieure à la {1}".sprintf(willHandleDate?"date de fin":"valeur supérieure",willHandleDate?"date de début":"valeur inférieure"));
}
if(typeof(success) =="function"){
success(data.start+"=>"+data.end);
}
resolve(data.start+"=>"+data.end);
DialogProvider.close();
return true;
}
})
})
}
setAction(action,text){
if(!(this.searchFilter.current)) return;
if(action === this.state.action && !periodActions[action] && !betweenActions[action]) return;
let value = this.state.defaultValue;
let act = defaultStr(action).toLowerCase();
const isDateTime = this.type?.contains("time");
const dateFormat = isDateTime?DateLib.SQLDateTimeFormat:DateLib.SQLDateFormat;
if(action == '$period'){
this.showPeriodSelector((d)=>{
this.runAction({value:d,action});
})
} else if(action == '$between'){
this.showBetweenSelector((d)=>{
this.runAction({value:d,action});
})
} else if(action =="$today"){
return this.runAction({value:new Date().resetHours().resetMinutes().resetSeconds().toFormat(dateFormat),action})
} else if(action =="$yesterday"){
return this.runAction({value: DateLib.removeDays(1,new Date(),null,true).resetHours().resetMinutes().resetSeconds().toFormat(dateFormat),action})
} else if(act.startsWith("$") && (act.contains("week") || act.contains("month"))){
let diff = undefined;
const currentDate = new Date();
currentDate.setHours(0);
currentDate.setMinutes(0);
currentDate.setSeconds(0);
switch (action){
case "$month":
diff = DateLib.currentMonthDaysLimits(currentDate);
break;
case "$week":
diff = DateLib.currentWeekDaysLimits(currentDate);
break;
case "$prevWeek":
diff = DateLib.previousWeekDaysLimits(currentDate)
break;
}
if(diff){
let value = diff.first.toFormat(dateFormat) +"=>"+diff.last.toFormat(dateFormat);
this.runAction({value,action})
}
} else {
this.runAction({value,action});
}
}
setOperator(op,text){
if(this.state.operator === op) return;
this.setState({operator:op},()=>{
this.fireValueChanged();
this.callOnValidate();
});
}
clearText(cb){
this.setState({
emptyTextIcon : false,
defaultValue : undefined,
},()=>{
this.callOnValidate();
if(isFunction(cb)){
cb();
}
this.fireValueChanged();
})
}
clearFilter(event){
this.setState({defaultValue:undefined,action:this.getDefaultAction()},()=>{
this.callOnValidate();
this.fireValueChanged(true);
let {onClearFilter,onResetFilter} = this.props;
onClearFilter = defaultVal(onClearFilter,onResetFilter);
if(isFunction(onClearFilter)){
onClearFilter.call(this,{name:this.name,field:this.name,type:this.type,context:this,props:this.props});
}
})
}
isBetweenAction(action){
action = defaultStr(action,this.state.action).toLowerCase();
return !!(this.state.actions && betweenActions[action]);
}
isPeriodAction(action){
action = defaultStr(action,this.state.action).toLowerCase();
return !!(this.state.actions && periodActions[action]);
}
formatValue(value){
const type = defaultStr(this.type,this.props.type).toLowerCase();
if(this.isBetweenAction() && isNonNullString(value) && (type ==="number" || type =="decimal")){
const sp = value.split("=>");
if(sp.length ==2 && type){
const format = defaultStr(this.props.format).toLowerCase();
const v1 = defaultDecimal(parseDecimal(sp[0],type));
const v2 = defaultDecimal(parseDecimal(sp[1],type));
return format =="money"? (v1.formatMoney()+"=>"+v2.formatMoney()) : (v1.formatNumber()+"=>"+v2.formatNumber());
}
}
if(typeof value =='number'){
if(this.props.format =='money'){
return value.formatMoney();
}
return value.formatNumber();
}
return value;
}
UNSAFE_componentWillReceiveProps(nexProps){
const state = {};
const defaultValue = nexProps.defaultValue == null || nexProps.defaultValue =="" ? undefined : nexProps.defaultValue;
const stateValue = this.state.defaultValue == null || this.state.defaultValue ==""? undefined : this.state.defaultValue;
if('defaultValue' in nexProps && defaultValue != stateValue){
state.defaultValue = defaultValue;
}
if(isNonNullString(nexProps.operator) && nexProps.operator in this.state.operators){
state.operator = nexProps.operator;
}
if(isNonNullString(nexProps.action) in nexProps && nexProps.action in this.state.actions){
state.action = nexProps.action;
}
if(Object.size(state,true)){
this.setState(state,()=>{});
}
}
render (){
let {
filter,
label,
withLabel,
text,
tooltip,
dynamicRendered,
tooltipLabel,
andOperator,
orOperator,
searchIcon,
field,
style,
anchorProps,
//mode,
//inputProps,
moreOptions,
isLoading,
searchIconTooltip,
withBottomSheet,
render,
ref,
data,
testID,
filterContainerProps,
responsiveProps,
...rest
} = {...this.props,...this.filterProps};
const type = this.type;
if(filter === false || ((andOperator === false && orOperator === false) || type ==='image')) return null;
label = defaultStr(label,text,tooltip,tooltipLabel,field)
const {defaultValue,actions,action,operator,operators} = this.state;
searchIcon = React.isValidElement(searchIcon)?searchIcon : searchIcon ? <Icon title={searchIconTooltip} icon={searchIcon}/> : null;
let hasFilterVal = !isNullOrEmpty(defaultValue,true);
rest.label = label;
const activeColor = theme.colors.primaryOnSurface;
const activeStyle = {color:activeColor};
const manualRun = this.willRunManually();
testID = defaultStr(testID,'RN_FilterComponent_'+this.name);
const options = {
go : {
text : 'Rechercher (Go)',
icon : 'magnify',
onPress : ()=>{
this.fireValueChanged(true);
},
},
divider:{divider:true},
manual : {
text : 'Recherche manuelle',
icon : manualRun ? 'check': null,
style : manualRun? activeStyle : null,
onPress : this.toggleManualRun.bind(this)
},
};
if(this.state.isTextFilter){
options.ignoreCase = {
text : 'Ignorer la casse',
icon : this.state.ignoreCase ? 'check':null,
style : this.state.ignoreCase ? activeStyle : null,
onPress : this.toggleIgnoreCase.bind(this),
}
}
if(typeof moreOptions ==='function'){
moreOptions = moreOptions({context:this,type:this.type,props:rest});
}
let hasOptions = false;
if(typeof moreOptions =='object' && moreOptions){
Object.map(moreOptions,(o,i)=>{
if(!isObj(o)) return null;
const label = defaultStr(o.label,o.text);
if(label){
if(!hasOptions){
hasOptions = true;
options.moreoptsDivieer = {divider:true};
}
options["more-"+i] = o;
}
})
}
rest.name = this.name;
rest.validate = false;
rest.formName = uniqid("form-name-field-select");
rest.renderfilter = "true"; //pour préciser que c'est un filtre de données
if(isFunction(filter)){
rest.filter = filter;
}
const isPeriodAction = this.isPeriodAction();
const isBetweenAction = this.isBetweenAction();
const ignoreDefaultValue = (isPeriodAction||isBetweenAction) && isNonNullString(defaultValue) && defaultValue.contains("=>");
rest.defaultValue = defaultValue;
rest.disabled = rest.readOnly = rest.affix = false;
rest.readOnly = false;
rest.style = [style];
rest.type = type;
const isMob = isMobileMedia() || withBottomSheet;
isLoading = !!isLoading;
rest.pointerEvents = !isLoading ? "auto" : "none";
if(withLabel ===false){
if(type.contains("select")){
rest.dialogProps = defaultObj(rest.dialogProps);
rest.dialogProps.title = defaultStr(rest.dialogProps.title,label);
}
delete rest.label;
delete rest.text;
delete rest.title;
}
anchorProps = defaultObj(anchorProps);
rest.anchorProps = anchorProps;
rest.withLabel = withLabel;
rest.pointerEvents = "auto";
rest.contentContainerProps = {pointerEvents:"auto"};
rest.right = isLoading ? <ActivityIndicator color={theme.colors.secondaryOnSurface} animating/> :<>
<Menu
testID = {testID+"_Menu"}
sheet = {withBottomSheet}
anchor = {(props)=><Icon {...props} {...anchorProps} style={[theme.styles.noPadding,theme.styles.mt0,theme.styles.mb0,theme.styles.ml0,props.style,anchorProps.style]} primary={hasFilterVal} icon={hasFilterVal?'filter-menu':'filter-plus'}/>}
items = {[
{
text : !isMob ? 'Options' : ("Options de filtre ["+label+"]"),
icon : 'cog',
items : options,
style : styles.bold,
},
{divider:true},
...[hasFilterVal /*&& type !== 'select'*/?
{
text : 'Effacer le filtre',
icon : 'filter-remove',
onPress : this.clearFilter.bind(this),
}
:null],
...[isMob?{
text : 'Opérateurs',
closeOnPress : false,
style : [styles.bold,styles.noVerticalPadding],
}:null],
...Object.mapToArray(operators,(x,i)=>{
return {
text : x+' '+ (label?label.toLowerCase():'')+' ',
icon : i === operator ? 'check' : null,
style : i === operator ? activeStyle : null,
onPress : (e)=>{React.stopEventPropagation(e);this.setOperator(i,x);return false;},
}
}),
{divider:true},
...[isMob?{
text : 'Actions',
closeOnPress : false,
style : [styles.bold,styles.noVerticalPadding],
}:null],
...Object.mapToArray(actions,(x,j)=>{
if(ignoreDefaultValue && !periodActions[j] && !betweenActions[j]){
return null;
}
let checked = j === action?true : false;
if(checked && (isNumber(defaultValue) || isNonNullString(defaultValue))) {
let hasS = false;
let act = defaultStr(action).toLowerCase();
if(act =="$today" || act =='$yesterday'){
} else if((action =="$period" || (act.startsWith("$") && (act.contains("week") || act.contains("month"))))){
let sp = defaultValue.split("=>");
x = DateLib.formatDatePeriod(defaultValue,this.isDateTime());
if(!x){
if((DateLib.isValidSQLDate(sp[0])|| DateLib.isValidSQLDateTime(sp[0])) && (DateLib.isValidSQLDate(sp[1]) || DateLib.isValidSQLDateTime(sp[1]))){
x = "Du "+defaultStr(DateLib.format(sp[0],DateLib.defaultDateFormat),sp[0])+" au "+defaultStr(DateLib.format(sp[1],DateLib.defaultDateFormat),sp[1]);
hasS = true;
}
} else {
hasS = true;
}
}
if(!hasS){
x = x+" <"+this.formatValue(defaultValue)+">"
}
}
return {
label : x,
icon : checked ? 'check' : null,
style : checked ? activeStyle : null,
onPress : (e)=>{React.stopEventPropagation(e);this.setAction(j,x);return false}}
}
)
]}
/>
</>
const containerProps = defaultObj(this.props.containerProps,rest.containerProps);
delete rest.containerProps;
const Component = isBetweenAction ? FilterBetweenComponent : this.Component;
responsiveProps = Object.assign({},responsiveProps);
responsiveProps.style = [theme.styles.w100,responsiveProps.style]
if(ignoreDefaultValue && isPeriodAction) {
rest.isPeriodAction = true;
}
return <View testID={testID+"_FilterContainer"} {...containerProps} style={StyleSheet.flatten([theme.styles.w100,containerProps.style])}>
<Component
{...rest}
readOnly = {ignoreDefaultValue}
responsiveProps = {responsiveProps}
isFilter
name = {this.name}
testID = {testID}
onValidate = {this.onFilterValidate.bind(this)}
onChange = {x=>null}
ref = {React.mergeRefs(this.searchFilter,ref)}
/>
</View>
}
}
const styles = StyleSheet.create({
noVerticalPadding : {
//paddingVertical:0,
//marginVertical:0,
paddingTop : 5,
marginHorizontal : 10,
//height : 45,
},
bold : {
fontWeight :'bold'
}
})
/***** Les filtres prenent en paramètre :
*
* le nom d'une colonne,
* un opérateur et une action
*/
/**** lors du rendu d'un composant de type Fild, si la valeur renderFilter est définie alors l'utilisateur doit prendre en compte qu'il
* s'agit du rendu d'un champ de filtre
*/
Filter.propTypes = {
withLabel : PropTypes.bool,//si le rendu des filtres prendra en compte le label
moreOptions : PropTypes.oneOfType([
PropTypes.object,
PropTypes.array,
PropTypes.func
]),
dynamicRendered : PropTypes.bool,//si le filtre est rendu dynamiquement
isLoading : PropTypes.bool,
/*** si l'opérateur or sera accepté */
orOperator : PropTypes.bool,
///si l'opérateur and sera accepté
andOperator : PropTypes.bool,
/*** spécifie si la casse sera ignorée où non */
ignoreCase : PropTypes.bool,
operator : PropTypes.string, //l'opérateur par défaut : and, or,
action : PropTypes.oneOfType([PropTypes.string,PropTypes.bool]), //l'action par défaut : supérieur, supérieur où égal, est dans, ...
searchIcon : PropTypes.oneOfType([PropTypes.string,PropTypes.node]),
/*** l'info bulle de l'icone de recherche */
searchIconTooltip : PropTypes.string,
field : PropTypes.string.isRequired,
/**
* La foncton de rappel appelée en cas de mise à jour du champ
* onChange(value,{action,operator,field,value,selector})
*/
onChange : PropTypes.func,
/**** lorsque le contenu du filtre est réinitialisé */
onClearFilter : PropTypes.func,
onResetFilter : PropTypes.func, //idem à onClearFilter
}
/****
compare
*/
export const compareValues = (v1,v2)=>{
if(v1 === v2) return true;
if(Array.isArray(v1) && v1.length ==0 || v1 === null || v1 =="" || String(v1).trim() =="") v1 = undefined;
if(Array.isArray(v2) && v2.length ==0 || v2 === null || v2 == "" || String(v2).trim() =="") v2 = undefined;
return v1 === v2 || JSON.stringify(v1) === JSON.stringify(v2);
}
Filter.compareValues = compareValues;