aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/FilterPanel.tsx
diff options
context:
space:
mode:
authorSophie Zhang <sophie_zhang@brown.edu>2023-09-18 17:40:01 -0400
committerSophie Zhang <sophie_zhang@brown.edu>2023-09-18 17:40:01 -0400
commit013f25f01e729feee5db94900c61f4be4dd46869 (patch)
tree765dd5f2e06d6217ca79438e1098cefc8da627bf /src/client/views/FilterPanel.tsx
parentf5e765adff1e7b32250eb503c9724a4ac99117f3 (diff)
parent84aa8806a62e2e957e8281d7d492139e3d8225f2 (diff)
Merge branch 'master' into sophie-report-manager
Diffstat (limited to 'src/client/views/FilterPanel.tsx')
-rw-r--r--src/client/views/FilterPanel.tsx382
1 files changed, 313 insertions, 69 deletions
diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx
index 68d29942b..69ceb0f65 100644
--- a/src/client/views/FilterPanel.tsx
+++ b/src/client/views/FilterPanel.tsx
@@ -1,24 +1,32 @@
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 { StrCast } from '../../fields/Types';
+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 { SearchBox } from './search/SearchBox';
-import { undoable } from '../util/UndoManager';
-import { AiOutlineMinusSquare } from 'react-icons/ai';
-import { CiCircleRemove } from 'react-icons/ci';
+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<filterProps> {
+ private _documentOptions: DocumentOptions = new DocumentOptions();
+
public static LayoutString(fieldKey: string) {
return FieldView.LayoutString(FilterPanel, fieldKey);
}
@@ -41,43 +49,77 @@ export class FilterPanel extends React.Component<filterProps> {
const allDocs = new Set<Doc>();
const targetDoc = this.targetDoc;
if (targetDoc) {
- SearchBox.foreachRecursiveDoc([this.targetDoc], (depth, doc) => allDocs.add(doc));
+ 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'];
+ const noviceReqFields = ['author', 'tags', 'text', 'type', '-linkedTo'];
const noviceLayoutFields: string[] = []; //["_layout_curPage"];
const noviceFields = [...noviceReqFields, ...noviceLayoutFields];
const keys = new Set<string>(noviceFields);
- this.allDocs.forEach(doc => SearchBox.documentKeys(doc).filter(key => keys.add(key)));
+ 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));
+ }
+
/**
- * The current attributes selected to filter based on
+ * 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);
+ return StrListCast(this.targetDoc?._childFilters).concat(this.rangeFilters);
}
+ @computed get mapActiveFiltersToFacets() {
+ const filters = new Map<string, string>();
+ //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]);
- }
+ // @computed get currentFacets() {
+ // return this.activeFilters.map(filter => filter.split(Doc.FilterSep)[0]);
+ // }
gatherFieldValues(childDocs: Doc[], facetKey: string) {
- const valueSet = new Set<string>();
+ const valueSet = new Set<string>(StrListCast(this.props.rootDoc.childFilters).map(filter => filter.split(Doc.FilterSep)[1]));
let rtFields = 0;
let subDocs = childDocs;
if (subDocs.length > 0) {
@@ -99,6 +141,7 @@ export class FilterPanel extends React.Component<filterProps> {
}
// }
// });
+
return { strings: Array.from(valueSet.keys()), rtFields };
}
@@ -107,46 +150,93 @@ export class FilterPanel extends React.Component<filterProps> {
Doc.setDocRangeFilter(this.targetDoc, filterName, undefined);
};
- @observable _chosenFacets = new ObservableMap<string, 'text' | 'checkbox' | 'slider' | 'range'>();
- @computed get activeFacets() {
- const facets = new Map<string, 'text' | 'checkbox' | 'slider' | 'range'>(this._chosenFacets);
- StrListCast(this.targetDoc?._childFilters).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 _chosenFacets = new ObservableMap<string, 'text' | 'checkbox' | 'slider' | 'range'>();
+ @observable _chosenFacetsCollapse = new ObservableMap<string, boolean>();
+ @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<string>();
+
/**
- * Responds to clicking the check box in the flyout menu
+ * 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) => {
- if (!this.targetDoc) return;
- const allCollectionDocs = this.targetDocChildren;
- const facetValues = this.gatherFieldValues(this.targetDocChildren, facetHeader);
+ // just when someone chooses a facet
- 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);
+ 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)));
}
});
- 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) {
- } else {
- this._chosenFacets.set(facetHeader, 'checkbox');
+
+ for (var key of this.facetValues(facetHeader)) {
+ if (this.mapActiveFiltersToFacets.get(key)) {
+ this._collapseReturnKeys.push(key);
+ }
}
+
+ return <div className=" filterbox-collpasedAndActive">{this._collapseReturnKeys.join(', ')}</div>;
};
facetValues = (facetHeader: string) => {
const allCollectionDocs = new Set<Doc>();
- SearchBox.foreachRecursiveDoc(this.targetDocChildren, (depth: number, doc: Doc) => allCollectionDocs.add(doc));
- const set = new Set<string>([String.fromCharCode(127) + '--undefined--']);
+ SearchUtil.foreachRecursiveDoc(this.targetDocChildren, (depth: number, doc: Doc) => allCollectionDocs.add(doc));
+ const set = new Set<string>([...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])
@@ -156,8 +246,11 @@ export class FilterPanel extends React.Component<filterProps> {
else
allCollectionDocs.forEach(child => {
const fieldVal = child[facetHeader] as Field;
- set.add(Field.toString(fieldVal));
- (fieldVal === true || fieldVal === false) && set.add((!fieldVal).toString());
+ 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);
@@ -168,14 +261,55 @@ export class FilterPanel extends React.Component<filterProps> {
};
render() {
- const options = this._allFacets.filter(facet => this.currentFacets.indexOf(facet) === -1).map(facet => ({ value: facet, label: facet }));
- console.log("this is option " + options)
- console.log("this is alll facets " + this._allFacets)
+ 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 (
<div className="filterBox-treeView">
<div className="filterBox-select">
<div style={{ width: '100%' }}>
- <Select placeholder="Add a filter..." options={options} isMulti={false} onChange={val => this.facetClick((val as UserOptions).value)} onKeyDown={e => e.stopPropagation()} value={null} closeMenuOnSelect={true} />
+ <Select
+ styles={{
+ control: (baseStyles, state) => ({
+ ...baseStyles,
+ color: SettingsManager.userColor,
+ background: SettingsManager.userBackgroundColor,
+ }),
+ placeholder: (baseStyles, state) => ({
+ ...baseStyles,
+ color: SettingsManager.userColor,
+ background: SettingsManager.userBackgroundColor,
+ }),
+ input: (baseStyles, state) => ({
+ ...baseStyles,
+ color: SettingsManager.userColor,
+ background: SettingsManager.userBackgroundColor,
+ }),
+ option: (baseStyles, state) => ({
+ ...baseStyles,
+ color: SettingsManager.userColor,
+ background: !state.isFocused ? SettingsManager.userBackgroundColor : SettingsManager.userVariantColor,
+ }),
+ menuList: (baseStyles, state) => ({
+ ...baseStyles,
+ backgroundColor: SettingsManager.userBackgroundColor,
+ }),
+ }}
+ placeholder="Add a filter..."
+ options={options}
+ isMulti={false}
+ onChange={val => this.facetClick((val as UserOptions).value)}
+ onKeyDown={e => e.stopPropagation()}
+ value={null}
+ closeMenuOnSelect={true}
+ />
</div>
{/* THE FOLLOWING CODE SHOULD BE DEVELOPER FOR BOOLEAN EXPRESSION (AND / OR) */}
{/* <div className="filterBox-select-bool">
@@ -190,38 +324,72 @@ export class FilterPanel extends React.Component<filterProps> {
</div>
<div className="filterBox-tree" key="tree">
- {Array.from(this.activeFacets.keys()).map(facetHeader => (
- <div>
- <div className = "filterBox-facetHeader">
- <div className = "filterBox-facetHeader-Header"> </div>
- {facetHeader.charAt(0).toUpperCase() + facetHeader.slice(1)}
-
- <div className = "filterBox-facetHeader-collapse">
- <AiOutlineMinusSquare/>
- {/* <CiCircleRemove/> */}
- </div>
-
- </div>
-
+ {Array.from(this.activeRenderedFacetInfos.keys()).map(
+ (
+ renderInfo // iterato over activeFacetRenderInfos ==> renderInfo which you can renderInfo.facetHeader
+ ) => (
+ <div>
+ <div className="filterBox-facetHeader">
+ <div className="filterBox-facetHeader-Header"> </div>
+ {renderInfo.facetHeader.charAt(0).toUpperCase() + renderInfo.facetHeader.slice(1)}
- {this.displayFacetValueFilterUIs(this.activeFacets.get(facetHeader), facetHeader)}
- </div>
- ))}
+ <div
+ className="filterBox-facetHeader-collapse"
+ onClick={action(e => {
+ const collapseBoolValue = this._chosenFacetsCollapse.get(renderInfo.facetHeader);
+ this._chosenFacetsCollapse.set(renderInfo.facetHeader, !collapseBoolValue);
+ })}>
+ {this._chosenFacetsCollapse.get(renderInfo.facetHeader) ? <AiOutlinePlusSquare /> : <AiOutlineMinusSquare />}
+ </div>
+
+ <div
+ className="filterBox-facetHeader-remove"
+ onClick={action(e => {
+ 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');
+ }
+ })}>
+ <CiCircleRemove />{' '}
+ </div>
+ </div>
+
+ {this._chosenFacetsCollapse.get(renderInfo.facetHeader)
+ ? this.sortingCurrentFacetValues(renderInfo.facetHeader)
+ : this.displayFacetValueFilterUIs(renderInfo.renderType, renderInfo.facetHeader, renderInfo.domain, renderInfo.range)}
+ {/* */}
+ </div>
+ )
+ )}
</div>
</div>
);
}
- private displayFacetValueFilterUIs(type: string | undefined, facetHeader: string): React.ReactNode {
+ private displayFacetValueFilterUIs(type: string | undefined, facetHeader: string, renderInfoDomain?: number[] | undefined, renderInfoRange?: number[]): React.ReactNode {
switch (type) {
case 'text':
return (
<input
- placeholder={
+ key={this.targetDoc[Id]}
+ placeholder={'enter text to match'}
+ defaultValue={
StrListCast(this.targetDoc._childFilters)
.find(filter => filter.split(Doc.FilterSep)[0] === facetHeader)
- ?.split(Doc.FilterSep)[1] ?? '-empty-'
+ ?.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)}
/>
@@ -233,11 +401,11 @@ export class FilterPanel extends React.Component<filterProps> {
<div>
<input
style={{ width: 20, marginLeft: 20 }}
- checked={
+ checked={['check', 'exists'].includes(
StrListCast(this.targetDoc._childFilters)
.find(filter => filter.split(Doc.FilterSep)[0] === facetHeader && filter.split(Doc.FilterSep)[1] == facetValue)
- ?.split(Doc.FilterSep)[2] === 'check'
- }
+ ?.split(Doc.FilterSep)[2] ?? ''
+ )}
type={type}
onChange={undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, fval, e.target.checked ? 'check' : 'remove'), 'set filter')}
/>
@@ -245,6 +413,82 @@ export class FilterPanel extends React.Component<filterProps> {
</div>
);
});
+
+ 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 (
+ <>
+ {/* <div className="sliderBox-outerDiv-checkBox" style={{ float: 'left' }}>
+ <Checkbox color="primary" onChange={action(() => console.log('on change'))} />
+ </div> */}
+
+ <div className="sliderBox-outerDiv" style={{ width: '95%', height: 45, float: 'right' }}>
+ <Slider
+ mode={2}
+ step={Math.min(1, 0.1 * (domain[1] - domain[0]))}
+ domain={[domain[0], domain[1]]} // -1000, 1000
+ rootStyle={{ position: 'relative', width: '100%' }}
+ onChange={values => Doc.setDocRangeFilter(this.targetDoc, facetHeader, values)}
+ values={renderInfoRange!}>
+ <Rail>{railProps => <TooltipRail {...railProps} />}</Rail>
+ <Handles>
+ {({ handles, activeHandleID, getHandleProps }) => (
+ <div className="slider-handles">
+ {handles.map((handle, i) => {
+ // const value = i === 0 ? defaultValues[0] : defaultValues[1];
+ return (
+ <div>
+ <Handle key={handle.id} handle={handle} domain={domain} isActive={handle.id === activeHandleID} getHandleProps={getHandleProps} />
+ </div>
+ );
+ })}
+ </div>
+ )}
+ </Handles>
+ <Tracks left={false} right={false}>
+ {({ tracks, getTrackProps }) => (
+ <div className="slider-tracks">
+ {tracks.map(({ id, source, target }) => (
+ <Track key={id} source={source} target={target} disabled={false} getTrackProps={getTrackProps} />
+ ))}
+ </div>
+ )}
+ </Tracks>
+ <Ticks count={5}>
+ {({ ticks }) => (
+ <div className="slider-ticks">
+ {ticks.map(tick => (
+ <Tick key={tick.id} tick={tick} count={ticks.length} format={(val: number) => val.toString()} />
+ ))}
+ </div>
+ )}
+ </Ticks>
+ </Slider>
+ </div>
+ </>
+ );
+ }
+
+ // case 'range'
+ // return <Slider ...
+ // return <slider domain={renderInfo.domain}> domain is number[] for min and max
+ // onChange = { ... Doc.setDocRangeFilter(this.targetDoc, facetHeader, [extendedMinVal, extendedMaxVal] ) }
+ //
+ // OR
+
+ // return <div>
+ // <slider domain={renderInfo.domain}> // domain is number[] for min and max
+ // <dimain changing handles >
+ // <?div
}
}
}