import React = require('react'); import { action, computed, observable, ObservableMap } from 'mobx'; import { observer } from 'mobx-react'; import { Handles, Rail, Slider, Ticks, Tracks } from 'react-compound-slider'; import { AiOutlineMinusSquare, AiOutlinePlusSquare } from 'react-icons/ai'; import { CiCircleRemove } from 'react-icons/ci'; import Select from 'react-select'; import { Doc, DocListCast, Field, StrListCast } from '../../fields/Doc'; import { RichTextField } from '../../fields/RichTextField'; import { DocumentOptions, FInfo } from '../documents/Documents'; import { DocumentManager } from '../util/DocumentManager'; import { UserOptions } from '../util/GroupManager'; import { SearchUtil } from '../util/SearchUtil'; import { undoable } from '../util/UndoManager'; import './FilterPanel.scss'; import { FieldView } from './nodes/FieldView'; import { Handle, Tick, TooltipRail, Track } from './nodes/SliderBox-components'; import { SettingsManager } from '../util/SettingsManager'; import { Id } from '../../fields/FieldSymbols'; import { List } from '../../fields/List'; interface filterProps { rootDoc: Doc; } @observer export class FilterPanel extends React.Component { private _documentOptions: DocumentOptions = new DocumentOptions(); public static LayoutString(fieldKey: string) { return FieldView.LayoutString(FilterPanel, fieldKey); } /** * @returns the relevant doc according to the value of FilterBox._filterScope i.e. either the Current Dashboard or the Current Collection */ @computed get targetDoc() { return this.props.rootDoc; } @computed get targetDocChildKey() { const targetView = DocumentManager.Instance.getFirstDocumentView(this.targetDoc); return targetView?.ComponentView?.annotationKey ?? targetView?.ComponentView?.fieldKey ?? 'data'; } @computed get targetDocChildren() { return [...DocListCast(this.targetDoc?.[this.targetDocChildKey] || Doc.ActiveDashboard?.data), ...DocListCast(this.targetDoc[Doc.LayoutFieldKey(this.targetDoc) + '_sidebar'])]; } @computed get allDocs() { const allDocs = new Set(); const targetDoc = this.targetDoc; if (targetDoc) { SearchUtil.foreachRecursiveDoc([this.targetDoc], (depth, doc) => allDocs.add(doc)); } console.log('this is all Docs' + Array.from(allDocs)); return Array.from(allDocs); } @computed get _allFacets() { // trace(); const noviceReqFields = ['author', 'tags', 'text', 'type', '-linkedTo']; const noviceLayoutFields: string[] = []; //["_layout_curPage"]; const noviceFields = [...noviceReqFields, ...noviceLayoutFields]; const keys = new Set(noviceFields); this.allDocs.forEach(doc => SearchUtil.documentKeys(doc).filter(key => keys.add(key))); const sortedKeys = Array.from(keys.keys()) .filter(key => key[0]) .filter(key => key.indexOf('modificationDate') !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith('_')) || noviceFields.includes(key) || !Doc.noviceMode) .sort(); // console.log('THIS IS HERE ' + Doc.UserDoc().color + 'space ' + Doc.UserDoc().color); noviceFields.forEach(key => sortedKeys.splice(sortedKeys.indexOf(key), 1)); console.log('this is novice fields ' + noviceFields + 'and this is sorted Keys ' + sortedKeys); return [...noviceFields, ...sortedKeys]; } @computed get rangeFilters() { return StrListCast(this.targetDoc?._childFiltersByRanges).filter((filter, i) => !(i % 3)); } /** * activeFilters( ) -- all filters that currently have a filter set on them in this document (ranges, and others) * ["#tags::bob::check", "tags::joe::check", "width", "height"] */ @computed get activeFilters() { return StrListCast(this.targetDoc?._childFilters).concat(this.rangeFilters); } @computed get mapActiveFiltersToFacets() { const filters = new Map(); //this.targetDoc.docFilters this.activeFilters.map(filter => filters.set(filter.split(Doc.FilterSep)[1], filter.split(Doc.FilterSep)[0])); return filters; } // // activeFacetHeaders() - just the facet names, not the rest of the filter // // this wants to return all the filter facets that have an existing filter set on them in order to show them in the rendered panel // this set may overlap the selectedFilters // if the components reloads, these will still exist and be shown // ["#tags", "width", "height"] // @computed get activeFacetHeaders() { const activeHeaders = new Array(); this.activeFilters.map(filter => activeHeaders.push(filter.split(Doc.FilterSep)[0])); return activeHeaders; } /** * @returns a string array of the current attributes */ // @computed get currentFacets() { // return this.activeFilters.map(filter => filter.split(Doc.FilterSep)[0]); // } gatherFieldValues(childDocs: Doc[], facetKey: string) { const valueSet = new Set(StrListCast(this.props.rootDoc.childFilters).map(filter => filter.split(Doc.FilterSep)[1])); let rtFields = 0; let subDocs = childDocs; if (subDocs.length > 0) { let newarray: Doc[] = []; while (subDocs.length > 0) { newarray = []; subDocs.forEach(t => { const facetVal = t[facetKey]; if (facetVal instanceof RichTextField || typeof facetVal === 'string') rtFields++; facetVal !== undefined && valueSet.add(Field.toString(facetVal as Field)); (facetVal === true || facetVal == false) && valueSet.add(Field.toString(!facetVal)); const fieldKey = Doc.LayoutFieldKey(t); const annos = !Field.toString(Doc.LayoutField(t) as Field).includes('CollectionView'); DocListCast(t[annos ? fieldKey + '_annotations' : fieldKey]).forEach(newdoc => newarray.push(newdoc)); annos && DocListCast(t[fieldKey + '_sidebar']).forEach(newdoc => newarray.push(newdoc)); }); subDocs = newarray; } } // } // }); return { strings: Array.from(valueSet.keys()), rtFields }; } public removeFilter = (filterName: string) => { Doc.setDocFilter(this.targetDoc, filterName, undefined, 'remove'); Doc.setDocRangeFilter(this.targetDoc, filterName, undefined); }; // @observable _chosenFacets = new ObservableMap(); @observable _chosenFacetsCollapse = new ObservableMap(); @observable _collapseReturnKeys = new Array(); // this computed function gets the active filters and maps them to their headers // // activeRenderedFacetInfos() // returns renderInfo for all user selected filters and for all existing filters set on the document // Map("tags" => {"checkbox"}, // "width" => {"rangs", domain:[1978,1992]}) // @computed get activeRenderedFacetInfos() { return new Set( Array.from(new Set(Array.from(this._selectedFacetHeaders).concat(this.activeFacetHeaders))).map(facetHeader => { const facetValues = this.gatherFieldValues(this.targetDocChildren, facetHeader); let nonNumbers = 0; let minVal = Number.MAX_VALUE, maxVal = -Number.MAX_VALUE; facetValues.strings.map(val => { const num = val ? Number(val) : Number.NaN; if (Number.isNaN(num)) { val && nonNumbers++; } else { minVal = Math.min(num, minVal); maxVal = Math.max(num, maxVal); } }); if (facetHeader === 'text') { return { facetHeader, renderType: 'text' }; } else if (facetHeader !== 'tags' && nonNumbers / facetValues.strings.length < 0.1) { const extendedMinVal = minVal - Math.min(1, Math.floor(Math.abs(maxVal - minVal) * 0.1)); const extendedMaxVal = Math.max(minVal + 1, maxVal + Math.min(1, Math.ceil(Math.abs(maxVal - minVal) * 0.05))); const ranged = Doc.readDocRangeFilter(this.targetDoc, facetHeader); // not the filter range, but the zooomed in range on the filter return { facetHeader, renderType: 'range', domain: [extendedMinVal, extendedMaxVal], range: ranged ? ranged : [extendedMinVal, extendedMaxVal] }; } else { return { facetHeader, renderType: 'checkbox' }; } }) ); } @observable _selectedFacetHeaders = new Set(); /** * user clicks on a filter facet because they want to see it. * this adds this chosen filter to a set of user selected filters called: selectedFilters * if this component reloads, then these filters will go away since they haven't been written to any Doc anywhere * * // this._selectedFacets.add(facetHeader); .. add to Set() not array */ @action facetClick = (facetHeader: string) => { // just when someone chooses a facet this._selectedFacetHeaders.add(facetHeader); return; }; @action sortingCurrentFacetValues = (facetHeader: string) => { this._collapseReturnKeys.splice(0); Array.from(this.activeRenderedFacetInfos.keys()).map(renderInfo => { if (renderInfo.renderType === 'range' && renderInfo.facetHeader === facetHeader && renderInfo.range) { this._collapseReturnKeys.push(renderInfo.range.map(number => number.toFixed(2))); } }); for (var key of this.facetValues(facetHeader)) { if (this.mapActiveFiltersToFacets.get(key)) { this._collapseReturnKeys.push(key); } } return
{this._collapseReturnKeys.join(', ')}
; }; facetValues = (facetHeader: string) => { const allCollectionDocs = new Set(); SearchUtil.foreachRecursiveDoc(this.targetDocChildren, (depth: number, doc: Doc) => allCollectionDocs.add(doc)); const set = new Set([...StrListCast(this.props.rootDoc.childFilters).map(filter => filter.split(Doc.FilterSep)[1]), Doc.FilterNone, Doc.FilterAny]); if (facetHeader === 'tags') allCollectionDocs.forEach(child => StrListCast(child[facetHeader]) .filter(h => h) .forEach(key => set.add(key)) ); else allCollectionDocs.forEach(child => { const fieldVal = child[facetHeader] as Field; if (!(fieldVal instanceof List)) { // currently we have no good way of filtering based on a field that is a list set.add(Field.toString(fieldVal)); (fieldVal === true || fieldVal === false) && set.add((!fieldVal).toString()); } }); const facetValues = Array.from(set).filter(v => v); let nonNumbers = 0; facetValues.map(val => Number.isNaN(Number(val)) && nonNumbers++); return nonNumbers / facetValues.length > 0.1 ? facetValues.sort() : facetValues.sort((n1: string, n2: string) => Number(n1) - Number(n2)); }; render() { let filteredOptions: string[] = ['author', 'tags', 'text', 'acl-Guest', ...this._allFacets.filter(facet => facet[0] === facet.charAt(0).toUpperCase())]; Object.entries(this._documentOptions).forEach((pair: [string, FInfo]) => { if (pair[1].filterable) { filteredOptions.push(pair[0]); } }); let options = filteredOptions.map(facet => ({ value: facet, label: facet })); return (
this.targetDoc && (this.targetDoc._childFilters_boolean = (e.target as any).value))} defaultValue={StrCast(this.targetDoc?.childFilters_boolean)}> {['AND', 'OR'].map(bool => ( ))}
{' '} */}
{Array.from(this.activeRenderedFacetInfos.keys()).map( ( renderInfo // iterato over activeFacetRenderInfos ==> renderInfo which you can renderInfo.facetHeader ) => (
{renderInfo.facetHeader.charAt(0).toUpperCase() + renderInfo.facetHeader.slice(1)}
{ const collapseBoolValue = this._chosenFacetsCollapse.get(renderInfo.facetHeader); this._chosenFacetsCollapse.set(renderInfo.facetHeader, !collapseBoolValue); })}> {this._chosenFacetsCollapse.get(renderInfo.facetHeader) ? : }
{ if (renderInfo.facetHeader === 'text') { Doc.setDocFilter(this.targetDoc, renderInfo.facetHeader, 'match', 'remove'); } else { for (var key of this.facetValues(renderInfo.facetHeader)) { if (this.mapActiveFiltersToFacets.get(key)) { Doc.setDocFilter(this.targetDoc, renderInfo.facetHeader, key, 'remove'); } } } this._selectedFacetHeaders.delete(renderInfo.facetHeader); this._chosenFacetsCollapse.delete(renderInfo.facetHeader); if (renderInfo.domain) { Doc.setDocRangeFilter(this.targetDoc, renderInfo.facetHeader, renderInfo.domain, 'remove'); } })}> {' '}
{this._chosenFacetsCollapse.get(renderInfo.facetHeader) ? this.sortingCurrentFacetValues(renderInfo.facetHeader) : this.displayFacetValueFilterUIs(renderInfo.renderType, renderInfo.facetHeader, renderInfo.domain, renderInfo.range)} {/* */}
) )}
); } private displayFacetValueFilterUIs(type: string | undefined, facetHeader: string, renderInfoDomain?: number[] | undefined, renderInfoRange?: number[]): React.ReactNode { switch (type) { case 'text': return ( filter.split(Doc.FilterSep)[0] === facetHeader) ?.split(Doc.FilterSep)[1] } style={{ color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }} onBlur={undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match'), 'set text filter')} onKeyDown={e => e.key === 'Enter' && undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match'), 'set text filter')(e)} /> ); case 'checkbox': return this.facetValues(facetHeader).map(fval => { const facetValue = fval; return (
filter.split(Doc.FilterSep)[0] === facetHeader && filter.split(Doc.FilterSep)[1] == facetValue) ?.split(Doc.FilterSep)[2] ?? '' )} type={type} onChange={undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, fval, e.target.checked ? 'check' : 'remove'), 'set filter')} /> {facetValue}
); }); case 'range': const domain = renderInfoDomain; const range = renderInfoRange; if (range) { console.log('this is info range ' + range[0] + ' , ' + range[1]); } if (domain) { console.log('this is info domain ' + domain[0] + ', ' + domain[1]); return ( <> {/*
console.log('on change'))} />
*/}
Doc.setDocRangeFilter(this.targetDoc, facetHeader, values)} values={renderInfoRange!}> {railProps => } {({ handles, activeHandleID, getHandleProps }) => (
{handles.map((handle, i) => { // const value = i === 0 ? defaultValues[0] : defaultValues[1]; return (
); })}
)}
{({ tracks, getTrackProps }) => (
{tracks.map(({ id, source, target }) => ( ))}
)}
{({ ticks }) => (
{ticks.map(tick => ( val.toString()} /> ))}
)}
); } // case 'range' // return domain is number[] for min and max // onChange = { ... Doc.setDocRangeFilter(this.targetDoc, facetHeader, [extendedMinVal, extendedMaxVal] ) } // // OR // return
// // domain is number[] for min and max // //