import React = require('react'); import { action, computed, observable, ObservableMap } from 'mobx'; import { observer } from 'mobx-react'; import Select from 'react-select'; import { Doc, DocListCast, Field, StrListCast } from '../../fields/Doc'; import { RichTextField } from '../../fields/RichTextField'; import { StrCast } from '../../fields/Types'; import { DocumentManager } from '../util/DocumentManager'; import { UserOptions } from '../util/GroupManager'; import './FilterPanel.scss'; import { FieldView } from './nodes/FieldView'; import { SearchBox } from './search/SearchBox'; import { undoable } from '../util/UndoManager'; import { AiOutlineMinusSquare, AiOutlinePlusSquare } from 'react-icons/ai'; import { CiCircleRemove } from 'react-icons/ci'; //slight bug when you don't click on background canvas before creating filter and the you click on the canvas interface filterProps { rootDoc: Doc; } @observer export class FilterPanel extends React.Component { 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) { SearchBox.foreachRecursiveDoc([this.targetDoc], (depth, doc) => allDocs.add(doc)); } return Array.from(allDocs); } @computed get _allFacets() { // trace(); const noviceReqFields = ['author', 'tags', 'text', 'type']; const noviceLayoutFields: string[] = []; //["_layout_curPage"]; const noviceFields = [...noviceReqFields, ...noviceLayoutFields]; const keys = new Set(noviceFields); this.allDocs.forEach(doc => SearchBox.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(); noviceFields.forEach(key => sortedKeys.splice(sortedKeys.indexOf(key), 1)); 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); } // // 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 currentActiveFilterz(){ // this.existingFilters const filters = new Map(); //this.targetDoc.docFilters this.activeFilters.map(filter => filters.set(filter.split(Doc.FilterSep)[1] ,filter.split(Doc.FilterSep)[0] )) console.log("what is wrong with filters " +filters ) return filters } /** * @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(); 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]; // console.log("facetVal " + facetVal) 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 activeFacets() { // selectedFacetRenderInfo => []:{facetName, renderInfo}[] renderInfo: { renderType: text|range|... , minVal?: , maxVal?; } // return new Set( // Array.from(new Set(Array.from(this.selectedFacetHeaders).concat(this.existingFacetHeaders))) // .map(facetHeader => { --> getting exisitng filters and new filters that havent been selected but need to remove duplicates // most of what facetClick did before ... // minVal, maxvval... // if (...) // return {key: facet, renderType: "text"} // else if (... numbers) return {key:facet, renderrType: range, extendedMinVal, maxval // return // return .. // })) // console.log("chosen collpase " + this._chosenFacetsCollapse) const facets = new Map(); this.activeFilters.map(filter => facets.set(filter.split(Doc.FilterSep)[0], filter.split(Doc.FilterSep)[2] === 'match' ? 'text' : 'checkbox')); // setTimeout(() => StrListCast(this.targetDoc?._childFilters).map(action(filter => this._chosenFacets.set(filter.split(Doc.FilterSep)[0], filter.split(Doc.FilterSep)[2] === 'match' ? 'text' : 'checkbox')))); return facets; } @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 // return; if (!this.targetDoc) return; const allCollectionDocs = this.targetDocChildren; 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' || (facetValues.rtFields / allCollectionDocs.length > 0.1 && facetValues.strings.length > 20)) { // this._chosenFacets.set(facetHeader, 'text'); } else if (facetHeader !== 'tags' && nonNumbers / facetValues.strings.length < 0.1) { console.log("in this IF STATEMENE ") const ranged = Doc.readDocRangeFilter(this.targetDoc, facetHeader); // not the filter range, but the zooomed in range on the filter 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))); // this.targetDoc["year-min"] = 1978 // // facetHeader == year // this.targetDoc[newFacetField + '-min'] = ranged === undefined ? extendedMinVal : ranged[0]; // newFacet[newFacetField + '-max'] = ranged === undefined ? extendedMaxVal : ranged[1]; // const scriptText = `setDocRangeFilter(this?.target, "${facetHeader}", range)`; Doc.setDocRangeFilter(this.targetDoc, facetHeader, [extendedMinVal, extendedMaxVal] ) // newFacet = Docs.Create.SliderDocument({ // title: facetHeader, // system: true, // target: targetDoc, // _fitWidth: true, // _height: 40, // _stayInCollection: true, // treeViewExpandedView: 'layout', // _treeViewOpen: true, // _forceActive: true, // _overflow: 'visible', // }); // const newFacetField = Doc.LayoutFieldKey(newFacet); // const ranged = Doc.readDocRangeFilter(targetDoc, facetHeader); // Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox // 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))); // newFacet[newFacetField + '-min'] = ranged === undefined ? extendedMinVal : ranged[0]; // newFacet[newFacetField + '-max'] = ranged === undefined ? extendedMaxVal : ranged[1]; // Doc.GetProto(newFacet)[newFacetField + '-minThumb'] = extendedMinVal; // Doc.GetProto(newFacet)[newFacetField + '-maxThumb'] = extendedMaxVal; // const scriptText = `setDocRangeFilter(this?.target, "${facetHeader}", range)`; // newFacet.onThumbChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, range: 'number' }); // newFacet.data = ComputedField.MakeFunction(`readNumFacetData(self.target, self, "${FilterBox.targetDocChildKey}", "${facetHeader}")`); } else { // this._chosenFacets.set(facetHeader, 'checkbox'); } this._chosenFacetsCollapse.set(facetHeader, false) }; @action sortingCurrentFacetValues = (facetHeader:string) => { this._collapseReturnKeys.splice(0) for (var key of this.facetValues(facetHeader)){ // console.log("key : " + key ) if (this.currentActiveFilterz.get(key)){ // console.log("WEREEE HERHEHHHHEHHHHEEE") this._collapseReturnKeys.push(key) }} return (
{this._collapseReturnKeys.join(', ') } {/* .toString()} */}
) } facetValues = (facetHeader: string) => { const allCollectionDocs = new Set(); SearchBox.foreachRecursiveDoc(this.targetDocChildren, (depth: number, doc: Doc) => allCollectionDocs.add(doc)); const set = new Set([String.fromCharCode(127) + '--undefined--']); 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; 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() { const options = this._allFacets.filter(facet => this.currentFacets.indexOf(facet) === -1).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.activeFacets.keys()).map(facetHeader => ( // iterato over activeFacetRenderInfos ==> renderInfo which you can renderInfo.facetHeader
{facetHeader.charAt(0).toUpperCase() + facetHeader.slice(1)}
{ for (const [key, value] of this.currentActiveFilterz) { console.log("NEW KEY " + key + "NEW VAL " + value); } console.log("this is gather field values : " + this.gatherFieldValues(this.targetDocChildren, facetHeader)) console.log("this is current facets: " + this.currentFacets) console.log("this is facet Header " + facetHeader + ". this is the get " + this.activeFacets.get(facetHeader)) const collapseBoolValue = this._chosenFacetsCollapse.get(facetHeader) this._chosenFacetsCollapse.set(facetHeader, !collapseBoolValue )})}> {this._chosenFacetsCollapse.get(facetHeader) ? : }
{ for (var key of this.facetValues(facetHeader)){ if (this.currentActiveFilterz.get(key)){ Doc.setDocFilter(this.targetDoc, facetHeader, key, 'remove') }} this.activeFacets.delete(facetHeader) // this._chosenFacets.delete(facetHeader) this._chosenFacetsCollapse.delete(facetHeader) })} >
{/*
*/}
{ this._chosenFacetsCollapse.get(facetHeader) ? //
{this.sortingCurrentFacetValues(facetHeader)}
&& this._collapseReturnKeys.splice(0) this.sortingCurrentFacetValues(facetHeader) // && this._collapseReturnKeys.splice(0) : this.displayFacetValueFilterUIs(this.activeFacets.get(facetHeader), facetHeader) } // pass renderInfo from iterator {/* */}
))}
); } private displayFacetValueFilterUIs(type: string | undefined, facetHeader: string): React.ReactNode { // displayFacetValueFilterUIs(renderIinfo) switch (type /* renderInfo.type */ ) { case 'text': // if (this.chosenFacets.get(facetHeader) === 'text') return ( filter.split(Doc.FilterSep)[0] === facetHeader) ?.split(Doc.FilterSep)[1] ?? '-empty-' } 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] === 'check' } type={type} onChange={undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, fval, e.target.checked ? 'check' : 'remove'), 'set filter')} /> {facetValue}
); }) // 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 // //