aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/FilterPanel.tsx
diff options
context:
space:
mode:
authoreleanor-park <eleanor_park@brown.edu>2024-09-22 15:40:29 -0400
committereleanor-park <eleanor_park@brown.edu>2024-09-22 15:40:29 -0400
commit6d0cec80757dcdb07434d7788dd2eb97c2ce4b7c (patch)
tree58bc45587534b17a5bb340e8e5bafc47a749f14f /src/client/views/FilterPanel.tsx
parent692076b1356309111c4f2cb69cbdbf4be1a825bd (diff)
parent97c150a2b53ccb27921125d55c9cbf42abf3c588 (diff)
Merge branch 'eleanor-gptdraw' of https://github.com/brown-dash/Dash-Web into eleanor-gptdraw
Diffstat (limited to 'src/client/views/FilterPanel.tsx')
-rw-r--r--src/client/views/FilterPanel.tsx255
1 files changed, 234 insertions, 21 deletions
diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx
index b11fa3bd5..e34b66963 100644
--- a/src/client/views/FilterPanel.tsx
+++ b/src/client/views/FilterPanel.tsx
@@ -1,22 +1,161 @@
-/* eslint-disable react/jsx-props-no-spreading */
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tooltip } from '@mui/material';
import { action, computed, makeObservable, observable, ObservableMap } from 'mobx';
-import { observer } from 'mobx-react';
+import { observer, useLocalObservable } from 'mobx-react';
import * as React from 'react';
+import { useEffect, useRef } from '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 { Doc, DocListCast, Field, FieldType, LinkedTo, StrListCast } from '../../fields/Doc';
+import { DocData } from '../../fields/DocSymbols';
import { Id } from '../../fields/FieldSymbols';
import { List } from '../../fields/List';
import { RichTextField } from '../../fields/RichTextField';
+import { DocCast, StrCast } from '../../fields/Types';
+import { Button, CurrentUserUtils } from '../util/CurrentUserUtils';
import { SearchUtil } from '../util/SearchUtil';
import { SnappingManager } from '../util/SnappingManager';
import { undoable } from '../util/UndoManager';
import { FieldsDropdown } from './FieldsDropdown';
import './FilterPanel.scss';
import { DocumentView } from './nodes/DocumentView';
+import { ButtonType } from './nodes/FontIconBox/FontIconBox';
import { Handle, Tick, TooltipRail, Track } from './nodes/SliderBox-components';
import { ObservableReactComponent } from './ObservableReactComponent';
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+
+interface HotKeyButtonProps {
+ hotKey: Doc;
+ selected?: Doc;
+}
+
+/**
+ * Renders the buttons that correspond to each icon tag in the properties view. Allows users to change the icon,
+ * title, and delete.
+ */
+const HotKeyIconButton: React.FC<HotKeyButtonProps> = observer(({ hotKey /*, selected */ }) => {
+ const state = useLocalObservable(() => ({
+ isActive: false,
+ isEditing: false,
+ myHotKey: hotKey,
+
+ toggleActive() { this.isActive = !this.isActive; },
+ deactivate() { this.isActive = false; },
+ startEditing() { this.isEditing = true; },
+ stopEditing() { this.isEditing = false; },
+ setHotKey(newHotKey: string) { this.myHotKey.title = newHotKey; },
+ })); // prettier-ignore
+
+ const panelRef = useRef<HTMLDivElement>(null);
+ const inputRef = useRef<HTMLInputElement>(null);
+
+ const handleClick = () => state.toggleActive();
+
+ /**
+ * Updates the list of hotkeys based on the users input. replaces the old title with the new one and then assigns this new
+ * hotkey with the current icon
+ */
+ const updateFromInput = undoable(() => {
+ hotKey.title = StrCast(state.myHotKey.title);
+ hotKey.toolTip = `Click to toggle the ${StrCast(hotKey.title)}'s group's visibility`;
+ }, '');
+
+ /**
+ * Deselects if the user clicks outside the button
+ * @param event
+ */
+ const handleClickOutside = (event: MouseEvent) => {
+ if (panelRef.current && !panelRef.current.contains(event.target as Node)) {
+ state.deactivate();
+ if (state.isEditing) {
+ state.stopEditing();
+
+ updateFromInput();
+ }
+ }
+ };
+
+ useEffect(() => {
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
+ const iconOpts = ['star', 'heart', 'bolt', 'satellite', 'palette', 'robot', 'lightbulb', 'highlighter', 'book', 'chalkboard'] as IconProp[];
+
+ /**
+ * Panel of icons the user can choose from to represent their tag
+ */
+ const iconPanel = iconOpts.map(icon => (
+ <button
+ key={icon.toString()}
+ onClick={undoable(e => {
+ e.stopPropagation;
+ hotKey[DocData].icon = icon.toString();
+ }, '')}
+ className="icon-panel-button">
+ <FontAwesomeIcon icon={icon} color={SnappingManager.userColor} />
+ </button>
+ ));
+
+ /**
+ * Actually renders the buttons
+ */
+
+ return (
+ <div
+ className={`filterHotKey-button`}
+ onClick={e => {
+ e.stopPropagation();
+ state.startEditing();
+ setTimeout(() => inputRef.current?.focus(), 0);
+ }}>
+ <div className={`hotKey-icon-button ${state.isActive ? 'active' : ''}`} ref={panelRef}>
+ <Tooltip title={<div className="dash-tooltip">Click to customize this hotkey&apos;s icon</div>}>
+ <button
+ type="button"
+ className="hotKey-icon"
+ onClick={(e: React.MouseEvent) => {
+ e.stopPropagation();
+ handleClick();
+ }}>
+ <FontAwesomeIcon icon={hotKey.icon as IconProp} size="2xl" color={SnappingManager.userColor} />
+ </button>
+ </Tooltip>
+ {state.isActive && <div className="icon-panel">{iconPanel}</div>}
+ </div>
+ {state.isEditing ? (
+ <input
+ ref={inputRef}
+ type="text"
+ value={StrCast(state.myHotKey.title).toUpperCase()}
+ onChange={e => state.setHotKey(e.target.value)}
+ onBlur={() => {
+ state.stopEditing();
+ updateFromInput();
+ }}
+ onKeyDown={e => {
+ if (e.key === 'Enter') {
+ state.stopEditing();
+ updateFromInput();
+ }
+ }}
+ className="hotkey-title-input"
+ />
+ ) : (
+ <p className="hotkey-title">{StrCast(hotKey.title).toUpperCase()}</p>
+ )}
+ <button
+ className="hotKey-close"
+ onClick={(e: React.MouseEvent) => {
+ e.stopPropagation();
+ Doc.RemFromFilterHotKeys(hotKey);
+ }}>
+ <FontAwesomeIcon icon={'x' as IconProp} color={SnappingManager.userColor} />
+ </button>
+ </div>
+ );
+});
interface filterProps {
Document: Doc;
@@ -24,13 +163,16 @@ interface filterProps {
@observer
export class FilterPanel extends ObservableReactComponent<filterProps> {
- @observable _selectedFacetHeaders = new Set<string>();
+ // eslint-disable-next-line no-use-before-define
+ public static Instance: FilterPanel;
constructor(props: filterProps) {
super(props);
makeObservable(this);
+ FilterPanel.Instance = this;
}
+ @observable _selectedFacetHeaders = new Set<string>();
/**
* @returns the relevant doc according to the value of FilterBox._filterScope i.e. either the Current Dashboard or the Current Collection
*/
@@ -129,7 +271,7 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
@computed get activeRenderedFacetInfos() {
return new Set(
Array.from(new Set(Array.from(this._selectedFacetHeaders).concat(this.activeFacetHeaders))).map(facetHeader => {
- const facetValues = FilterPanel.gatherFieldValues(this.targetDocChildren, facetHeader, StrListCast(this.Document.childFilters));
+ const facetValues = facetHeader.startsWith('#') ? { strings: [] } : FilterPanel.gatherFieldValues(this.targetDocChildren, facetHeader, StrListCast(this.Document.childFilters));
let nonNumbers = 0;
let minVal = Number.MAX_VALUE;
@@ -147,7 +289,10 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
if (facetHeader === 'text') {
return { facetHeader, renderType: 'text' };
}
- if (facetHeader !== 'tags' && nonNumbers / facetValues.strings.length < 0.1) {
+ if (facetHeader.startsWith('#')) {
+ return { facetHeader, renderType: 'togglebox' };
+ }
+ if (facetHeader !== 'tags' && !facetHeader.startsWith('#') && 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: number[] | undefined = Doc.readDocRangeFilter(this.Document, facetHeader); // not the filter range, but the zooomed in range on the filter
@@ -192,21 +337,17 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
const allCollectionDocs = new Set<Doc>();
SearchUtil.foreachRecursiveDoc(this.targetDocChildren, (depth: number, doc: Doc) => allCollectionDocs.add(doc));
const set = new Set<string>([...StrListCast(this.Document.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 FieldType;
- 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());
- }
- });
+
+ allCollectionDocs.forEach(child => {
+ const fieldVal = child[facetHeader] as FieldType;
+ const fieldStrList = StrListCast(child[facetHeader]).filter(h => h);
+ if (fieldStrList.length) fieldStrList.forEach(key => set.add(key));
+ else 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;
@@ -215,12 +356,59 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
return nonNumbers / facetValues.length > 0.1 ? facetValues.sort() : facetValues.sort((n1: string, n2: string) => Number(n1) - Number(n2));
};
+ /**
+ * Allows users to add a filter hotkey to the properties panel. Will also update the multitoggle at the top menu and the
+ * icontags tht are displayed on the documents themselves
+ * @param hotKey tite of the new hotkey
+ */
+ addHotkey = (hotKey: string) => {
+ const buttons = DocCast(Doc.UserDoc().myContextMenuBtns);
+ const filter = DocCast(buttons.Filter);
+ const title = hotKey.startsWith('#') ? hotKey.substring(1) : hotKey;
+
+ const newKey: Button = {
+ title,
+ icon: 'question',
+ toolTip: `Click to toggle the ${title}'s group's visibility`,
+ btnType: ButtonType.ToggleButton,
+ expertMode: false,
+ toolType: '#' + title,
+ funcs: {},
+ scripts: { onClick: '{ return handleTags(this.toolType, _readOnly_);}' },
+ };
+
+ const newBtn = CurrentUserUtils.setupContextMenuBtn(newKey, filter);
+ newBtn.isSystem = newBtn[DocData].isSystem = undefined;
+
+ Doc.AddToFilterHotKeys(newBtn);
+ };
+
+ /**
+ * Renders the newly formed hotkey icon buttons
+ * @returns the buttons to be rendered
+ */
+ hotKeyButtons = () => {
+ const selected = DocumentView.SelectedDocs().lastElement();
+ const hotKeys = Doc.MyFilterHotKeys;
+
+ // Selecting a button should make it so that the icon on the top filter panel becomes said icon
+ const buttons = hotKeys.map(hotKey => (
+ <Tooltip key={StrCast(hotKey.title)} title={<div className="dash-tooltip">Click to customize this hotkey&apos;s icon</div>}>
+ <HotKeyIconButton hotKey={hotKey} selected={selected} />
+ </Tooltip>
+ ));
+
+ return buttons;
+ };
+
+ // @observable iconPanelMap: Map<string, number> = new Map();
+
render() {
return (
<div className="filterBox-treeView">
<div className="filterBox-select">
<div style={{ width: '100%' }}>
- <FieldsDropdown Document={this.Document} selectFunc={this.facetClick} showPlaceholder placeholder="add a filter" addedFields={['acl_Guest', LinkedTo]} />
+ <FieldsDropdown Document={this.Document} selectFunc={this.facetClick} showPlaceholder placeholder="add a filter" addedFields={['acl_Guest', LinkedTo, 'Star', 'Heart', 'Bolt', 'Cloud']} />
</div>
{/* THE FOLLOWING CODE SHOULD BE DEVELOPER FOR BOOLEAN EXPRESSION (AND / OR) */}
{/* <div className="filterBox-select-bool">
@@ -281,6 +469,15 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
)
)}
</div>
+ <div>
+ <div className="filterBox-select">
+ <div style={{ width: '100%' }}>
+ <FieldsDropdown Document={this.Document} selectFunc={this.addHotkey} showPlaceholder placeholder="add a hotkey" addedFields={['acl_Guest', LinkedTo]} />
+ </div>
+ </div>
+ </div>
+
+ <div>{this.hotKeyButtons()}</div>
</div>
);
}
@@ -321,6 +518,22 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
</div>
);
});
+ case 'togglebox':
+ return (
+ <div>
+ <input
+ style={{ width: 20, marginLeft: 20 }}
+ checked={['check', 'exists'].includes(
+ StrListCast(this.Document._childFilters)
+ .find(filter => filter.split(Doc.FilterSep)[0] === 'tags' && filter.split(Doc.FilterSep)[1] === facetHeader)
+ ?.split(Doc.FilterSep)[2] ?? ''
+ )}
+ type={'checkbox'}
+ onChange={undoable(e => Doc.setDocFilter(this.Document, 'tags', facetHeader, e.target.checked ? 'check' : 'remove'), 'set filter')}
+ />
+ -set-
+ </div>
+ );
case 'range':
{