Commit e3a81677 authored by Maksym Tishchenko's avatar Maksym Tishchenko
Browse files

Merge branch '405-filtering-by-inventory-group' into 'main'

Resolve "New accession and inventory filters"

Closes #413 and #405

See merge request grin-global/grin-global-ui!402
parents eff7a5f3 e98aad46
......@@ -47,6 +47,7 @@ class AccessionFilter {
public taxonomySpecies?: TaxonomySpeciesFilter;
public inventories?: InventoryFilter;
public sources?: AccessionSourceFilter;
public accessionInvGroup?: number[];
public accessionActions?: AccessionActionFilter;
public accessionIprs?: AccessionIprFilter;
public _text?: string;
......
import CooperatorFilter from '@gringlobal-ce/client/model/gringlobal/CooperatorFilter';
import DateFilter from '@gringlobal-ce/client/model/gringlobal/DateFilter';
import StringFilter from "@gringlobal-ce/client/model/common/StringFilter";
/**
* AccessionInvGroupFilter
......@@ -14,6 +15,7 @@ class AccessionInvGroupFilter {
public createdBy?: number[];
public createdDate?: DateFilter;
public modifiedBy?: number[];
public groupName?: StringFilter;
public modifiedDate?: DateFilter;
public ownedBy?: CooperatorFilter;
public ownedDate?: DateFilter;
......
import * as QueryString from 'query-string';
import { AxiosInstance, AxiosRequestConfig } from 'axios';
import AclSid from "@gringlobal-ce/client/model/acl/AclSid";
const URL_AUTOCOMPLETE_ACL_SID = `/api/v1/aclsid/autocomplete`;
/**
* AclSid service
*
* GRIN-Global CE API
*/
class AclSidService {
private _axios: AxiosInstance;
public constructor(axios: AxiosInstance) {
this._axios = axios;
}
/**
* autocompleteAclSid at /api/v1/aclsid/autocomplete
*
* @param term undefined
* @param xhrConfig additional xhr config
*/
public autocompleteAclSid = (term?: string, xhrConfig?: AxiosRequestConfig): Promise<AclSid[]> => {
const qs = QueryString.stringify({
term: term || undefined,
}, {});
const apiUrl = URL_AUTOCOMPLETE_ACL_SID + (qs ? `?${qs}` : '');
// console.log(`Fetching from ${apiUrl}`);
const content = { /* No content in request body */ };
return this._axios.request({
...xhrConfig,
url: apiUrl,
method: 'GET',
...content,
}).then(({ data }) => data as AclSid[]);
}
}
export default AclSidService;
......@@ -30,7 +30,7 @@ class OAuthManagementService {
* @param filter filter
* @param page = paged query
*/
public filter = (filter: string | OAuthClientFilter, page: IPageRequest, xhrConfig?: AxiosRequestConfig): Promise<OAuthClientFilteredPage> => {
public filter = (filter: string | Partial<OAuthClientFilter>, page: IPageRequest, xhrConfig?: AxiosRequestConfig): Promise<OAuthClientFilteredPage> => {
const qs = QueryString.stringify({
f: typeof filter === 'string' ? filter : undefined,
......
import AclSidService from "@gringlobal-ce/client/service/AclSidService";
import AuditLogService from '@gringlobal-ce/client/service/AuditLogService';
import CooperatorService from '@gringlobal-ce/client/service/CooperatorService';
import UserService from '@gringlobal-ce/client/service/UserService';
......@@ -108,6 +109,7 @@ export const reconfigureServiceAxios = ({ apiUrl, accessToken, origin, timeout,
const ConfiguredSecurityService = new SecurityService();
const ConfiguredUISecurity = makeUISecurity(ConfiguredSecurityService);
const ConfiguredAclSidService = new AclSidService(serviceAxios)
const ConfiguredAuditLogService = new AuditLogService(serviceAxios)
const ConfiguredTaxonomyService = new TaxonomyService(serviceAxios);
const ConfiguredCooperatorService = new CooperatorService(serviceAxios);
......@@ -151,6 +153,7 @@ const ConfiguredGenesysService = new GenesysService(serviceAxios)
export {
ConfiguredSecurityService as SecurityService,
ConfiguredUISecurity as UISecurity,
ConfiguredAclSidService as AclSidService,
ConfiguredAuditLogService as AuditLogService,
ConfiguredCooperatorService as CooperatorService,
ConfiguredUserService as UserService,
......
......@@ -50,6 +50,8 @@ interface IAutocompleteProps {
required?: boolean;
withDetails?: (details: any) => React.ReactNode;
autoFocus?: boolean;
onKeyPress?: (event) => void;
onBlur?: (event) => void;
}
interface IAutocompleteState {
......@@ -163,7 +165,7 @@ class Autocomplete extends React.Component<IAutocompleteProps & WithStyles & Wit
};
public render() {
const { label, classes, helperText, renderOption, getOptionLabel, noOptionsText, autoFocus, placeholder, t, required, meta, withDetails } = this.props;
const { label, classes, helperText, onKeyPress, onBlur, renderOption, getOptionLabel, noOptionsText, autoFocus, placeholder, t, required, meta, withDetails } = this.props;
const { error, touched, dirty } = meta;
const { loading, open, options, inputValue } = this.state;
......@@ -180,6 +182,8 @@ class Autocomplete extends React.Component<IAutocompleteProps & WithStyles & Wit
getOptionLabel={ getOptionLabel }
onChange={ this.onChange }
onInputChange={ this.onInputChange }
onKeyPress={ onKeyPress }
onBlur={ onBlur }
options={ options }
loading={ loading }
classes={ { input: classes.input, option: classes.option, popper: classes.popper } }
......
......@@ -4,6 +4,8 @@ import { useTranslation } from 'react-i18next';
// UI
import { withFilters, TextFilter, StringArrFilter, NumberFilter, DateFilter, BooleanFilter, CVFilter , SiteFilter } from 'ui/common/filter';
import AccessionInvGroupFilter from "common/AccessionInvGroupFilter";
import AclSidFilter from "common/AclSidFilter";
export function AccessionFiltersEmbedded({ prefix = '', withFullText = true }: { prefix?: string, withFullText?: boolean }) {
const { t } = useTranslation();
......@@ -77,6 +79,10 @@ export function AccessionFiltersEmbedded({ prefix = '', withFullText = true }: {
label={ t([ 'client:model.Accession.initialReceivedDate', 'client:model._.initialReceivedDate' ]) }
name={ `${prefix}initialReceivedDate` }
/>
<AccessionInvGroupFilter
label={ t([ 'client:model.AccessionInvGroup.groupName', 'client:model._.groupName' ]) }
name={ `${prefix}accessionInvGroup` }
/>
<BooleanFilter
label={ t(['client:model.AccessionSource.isOrigin', 'client:model._.isOrigin']) }
name={ `${prefix}sources.origin` }
......@@ -100,6 +106,22 @@ export function AccessionFiltersEmbedded({ prefix = '', withFullText = true }: {
label={ t(['client:model.Accession.isWebVisible', 'client:model._.isWebVisible']) }
name={ `${prefix}webVisible` }
/>
<AclSidFilter
label={ t('client:model._.createdBy') }
name={ `${prefix}createdBy` }
/>
<DateFilter
label={ t('client:model._.createdDate') }
name={ `${prefix}createdDate` }
/>
<AclSidFilter
label={ t('client:model._.modifiedBy') }
name={ `${prefix}modifiedBy` }
/>
<DateFilter
label={ t('client:model._.modifiedDate') }
name={ `${prefix}modifiedDate` }
/>
</>
);
}
......
import * as React from 'react';
import { createUseStyles } from 'react-jss';
import { HighlightOff as Clear } from '@material-ui/icons';
import { Field, FieldProps, useForm } from 'react-final-form';
import { FieldArray } from 'react-final-form-arrays';
// ui
import { WithStyles } from '@material-ui/core/styles';
import { InventoryService } from "@gringlobal-ce/client/service";
import {
AccessionInvGroup, AccessionInvGroupFilteredPage,
} from "@gringlobal-ce/client/model/gringlobal";
import Autocomplete from "@gringlobal-ce/client/ui/common/form/Autocomplete";
const useStyles = createUseStyles((theme) => ({
stringArrElement: {
margin: '.2rem 0',
padding: '.2rem 1rem',
backgroundColor: '#e8e5e1',
color: '#202222',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
},
stringArrElementText: {
display: 'inline-block',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
removeIcon: {
cursor: 'pointer',
color: '#6f6f6f',
fontSize: '20px',
},
}));
interface IAccessionInvGroupFilter extends FieldProps<any, any> {
name: string;
label: string;
placeholder?: string;
}
const StringArrElement = ({ arrElem, remove, classes }: { arrElem: string, remove: () => void } & WithStyles) => {
return <div>
<div className={ classes.stringArrElement }>
<span className={ classes.stringArrElementText }>
{arrElem}
</span>
<div className="font-bold float-right" onClick={ remove }><Clear className={ classes.removeIcon }/></div>
</div>
</div>
};
const AccessionInvGroupFilter = ({ name, label, ...other }: IAccessionInvGroupFilter) => {
// util
const classes = useStyles();
const { mutators, change, getFieldState } = useForm();
const [ groups, setGroups ] = React.useState([])
// effect
React.useEffect(() => {
const fieldValue = getFieldState(name)?.value
if (!fieldValue) {
return
}
InventoryService.filterGroups({ id: fieldValue }, { size: 500 }).then((page) => {
setGroups(page.content);
}).catch((err) => {
console.log('Could not fetch groups list', err);
});
}, []);
// callback
const maybeAdd = React.useCallback((val: AccessionInvGroup) => {
if (!val) {
return;
}
if (getFieldState(name)?.value?.length > 0 && getFieldState(name)?.value.includes(val.id)) {
change('temp', undefined);
return;
}
setGroups([ ...groups, val ])
mutators.push(name, val.id);
change('temp', undefined);
}, [ mutators, groups, setGroups, name ]);
const maybeRemove = React.useCallback((index) => {
mutators.remove(name, index);
}, [ groups, setGroups ])
const handleKeyPress = React.useCallback((event: KeyboardEvent) => {
if (event.key === 'Enter') {
if (getFieldState(`temp.${name}`).value) {
event.preventDefault();
maybeAdd(getFieldState(`temp.${name}`).value);
}
}
}, [ name, maybeAdd ]);
const renderOption = (invGroup: AccessionInvGroup, state?: any) => <>{ invGroup.groupName } </>
const optionLabel = (invGroup: AccessionInvGroup): string => invGroup.groupName;
const mapOptions = (page: AccessionInvGroupFilteredPage) => page.content;
const autocomplete = (text: string) => {
return InventoryService.filterGroups({ groupName: { contains: text } }, { page: 0, size: 20 });
};
const transformOption = (option: AccessionInvGroup | number[]): any => {
return typeof option[0] === 'number' ? {id: option[0]} : option;
}
return (
<>
<Field
name={ `temp.${name}` }
component={ Autocomplete }
label={ label }
autocomplete={ autocomplete }
renderOption={ renderOption }
mapOptions={ mapOptions }
transformOption={ transformOption }
getOptionLabel={ optionLabel }
onBlur={ () => maybeAdd(getFieldState(`temp.${name}`).value) }
onKeyPress={ handleKeyPress }
{ ...other }
/>
<FieldArray name={ name }>
{({ fields }) => (
<>
{fields?.value && fields.value.map((field, index) => {
return <StringArrElement
arrElem={ groups.find((group) => +group.id === +field)?.groupName ?? field }
remove={ () => maybeRemove(index) }
classes={ classes }
key={ `${field}-${index}` }
/>
})}
</>
)}
</FieldArray>
</>
);
}
export default AccessionInvGroupFilter;
import * as React from 'react';
import { createUseStyles } from 'react-jss';
import { HighlightOff as Clear } from '@material-ui/icons';
import { Field, FieldProps, useForm } from 'react-final-form';
import { FieldArray } from 'react-final-form-arrays';
// ui
import { WithStyles } from '@material-ui/core/styles';
import { AclSidService, OAuthManagementService, UserService } from "@gringlobal-ce/client/service";
import { OAuthClient, SysUser } from "@gringlobal-ce/client/model/gringlobal";
import Autocomplete from "@gringlobal-ce/client/ui/common/form/Autocomplete";
const useStyles = createUseStyles((theme) => ({
stringArrElement: {
margin: '.2rem 0',
padding: '.2rem 1rem',
backgroundColor: '#e8e5e1',
color: '#202222',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
},
stringArrElementText: {
display: 'inline-block',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
removeIcon: {
cursor: 'pointer',
color: '#6f6f6f',
fontSize: '20px',
},
}));
interface IAclSidFilter extends FieldProps<any, any> {
name: string;
label: string;
placeholder?: string;
}
const StringArrElement = ({ arrElem, remove, classes }: { arrElem: string, remove: () => void } & WithStyles) => {
return <div>
<div className={ classes.stringArrElement }>
<span className={ classes.stringArrElementText }>
{arrElem}
</span>
<div className="font-bold float-right" onClick={ remove }><Clear className={ classes.removeIcon }/></div>
</div>
</div>
};
const AclSidFilter = ({ name, label, endAdornment, ...other }: IAclSidFilter) => {
// util
const classes = useStyles();
const { mutators, change, getFieldState } = useForm();
const [ clientsData, setClientsData ] = React.useState([])
// effect
React.useEffect( () => {
const fieldValue = getFieldState(name)?.value
if (!fieldValue) {
return
}
getClientsData(fieldValue)
.catch((err) => {
console.log('Could not fetch users list', err);
})
}, []);
const getClientsData = async (fieldValue) => {
try {
const users = await UserService.listUsers({ id: fieldValue }, { size: 500 })
const clients = await OAuthManagementService.filter({ id: fieldValue }, { size: 500 })
setClientsData([ ...users.content, ...clients.content ]);
} catch (err) {
console.log('Could not fetch users list', err);
}
}
// callback
const maybeAdd = React.useCallback((val: SysUser) => {
if (!val) {
return;
}
if (getFieldState(name)?.value?.length > 0 && getFieldState(name)?.value.includes(val.id)) {
change('temp', undefined);
return;
}
setClientsData([ ...clientsData, val ])
mutators.push(name, val.id);
change('temp', undefined);
}, [ mutators, clientsData, setClientsData, name ]);
const maybeRemove = React.useCallback((index) => {
mutators.remove(name, index);
}, [ clientsData, setClientsData ])
const handleKeyPress = React.useCallback((event: KeyboardEvent) => {
if (event.key === 'Enter') {
if (getFieldState(`temp.${name}`).value) {
event.preventDefault();
maybeAdd(getFieldState(`temp.${name}`).value);
}
}
}, [ name, maybeAdd ]);
const renderOption = (data: SysUser | OAuthClient, state?: any) => <>{ data.fullName }</>
const optionLabel = (data: SysUser | OAuthClient): string => `${data.fullName}`;
const mapOptions = (page) => page;
const autocomplete = (text: string) => {
return AclSidService.autocompleteAclSid(text)
};
const transformOption = (option: SysUser | OAuthClient | number[]): any => {
return typeof option[0] === 'number' ? {id: option[0]} : option;
}
return (
<>
<Field
name={ `temp.${name}` }
component={ Autocomplete }
label={ label }
autocomplete={ autocomplete }
renderOption={ renderOption }
mapOptions={ mapOptions }
transformOption={ transformOption }
getOptionLabel={ optionLabel }
onBlur={ () => maybeAdd(getFieldState(`temp.${name}`).value) }
onKeyPress={ handleKeyPress }
{ ...other }
/>
<FieldArray name={ name }>
{({ fields }) => (
<>
{fields?.value && fields.value.map((field, index) => {
const client = clientsData.find((clientData) => +clientData.id === +field)
return <StringArrElement
arrElem={ client ? `${client.sid}` : field }
remove={ () => maybeRemove(index) }
classes={ classes }
key={ `${field}-${index}` }
/>
})}
</>
)}
</FieldArray>
</>
);
}
export default AclSidFilter;
......@@ -5,6 +5,8 @@ import { useTranslation } from 'react-i18next';
import { Inventory } from '@gringlobal-ce/client/model/gringlobal';
// UI
import { withFilters, TextFilter, StringArrFilter, NumberFilter, DateFilter, BooleanFilter, CVFilter, SiteFilter, IMPFilter } from 'ui/common/filter';
import AccessionInvGroupFilter from "common/AccessionInvGroupFilter";
import AclSidFilter from "common/AclSidFilter";
export function InventoryFiltersEmbedded({ prefix = '', allowSystem = true, withFullText = true }: { prefix?: string, allowSystem?: boolean, withFullText?: boolean }) {
const { t } = useTranslation();
......@@ -46,6 +48,10 @@ export function InventoryFiltersEmbedded({ prefix = '', allowSystem = true, with
label={ t([ 'client:model.Accession.accessionNumber', 'client:model._.accessionNumber' ]) }
name={ `${prefix}accession.accessionNumber` }
/>
<AccessionInvGroupFilter
label={ t([ 'client:model.AccessionInvGroup.groupName', 'client:model._.groupName' ]) }
name={ `${prefix}accessionInvGroup` }
/>
<NumberFilter
label={ t([ 'client:model.Inventory.quantityOnHand' ]) }
name={ `${prefix}quantityOnHand` }
......@@ -96,6 +102,22 @@ export function InventoryFiltersEmbedded({ prefix = '', allowSystem = true, with
label={ t([ 'client:model.Inventory.storageLocationPart4' ]) }
name={ `${prefix}storageLocationPart4.eq` }
/>
<AclSidFilter
label={ t('client:model._.createdBy') }
name={ `${prefix}createdBy` }
/>
<DateFilter
label={ t('client:model._.createdDate') }
name={ `${prefix}createdDate` }
/>
<AclSidFilter
label={ t('client:model._.modifiedBy') }
name={ `${prefix}modifiedBy` }
/>
<DateFilter
label={ t('client:model._.modifiedDate') }
name={ `${prefix}modifiedDate` }
/>
</>
);
}
......
......@@ -38,7 +38,7 @@ interface IDateFilter extends FieldProps<any, any> {
endAdornment?: any;
}
const validateDateFilter = (val) => !val || DateTime.fromFormat(val, 'yyyy-MM-dd').isValid ? undefined : 'Must be a valid date';
const validateDateFilter = (val) => !val || DateTime.fromISO(val).isValid ? undefined : 'Must be a valid date';
const DateFilter = ({ name, label, endAdornment, ...other }: IDateFilter) => {
......@@ -53,7 +53,7 @@ const DateFilter = ({ name, label, endAdornment, ...other }: IDateFilter) => {
const valueLe = getFieldState(`${name}.le`).value;
// console.log(`DateFilter, valueGe: ${valueGe}, valueLe: ${valueLe}`)
if (valueGe && valueLe && DateTime.fromFormat(valueGe, 'yyyy-MM-dd') > (DateTime.fromFormat(valueLe, 'yyyy-MM-dd'))) {
if (valueGe && valueLe && DateTime.fromISO(valueGe) > (DateTime.fromISO(valueLe))) {
// console.log('DateFilter, swapping')
change(`${name}.le`, valueGe);
change(`${name}.ge`, valueLe);
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment