diff options
| author | bobzel <zzzman@gmail.com> | 2024-09-30 11:22:45 -0400 | 
|---|---|---|
| committer | bobzel <zzzman@gmail.com> | 2024-09-30 11:22:45 -0400 | 
| commit | 04f1047d81bba00f9258543a8171683bce5272bb (patch) | |
| tree | 2e09704251382f554b0ea7e7c92e1e3a92b1b838 /src/client/views/FilterPanel.tsx | |
| parent | b08befda6d7ec07a0e6653ccf5040474886dcd44 (diff) | |
| parent | 22c1885a7469a6d5e94fff279225665a1ef1448b (diff) | |
merged with master
Diffstat (limited to 'src/client/views/FilterPanel.tsx')
| -rw-r--r-- | src/client/views/FilterPanel.tsx | 229 | 
1 files changed, 223 insertions, 6 deletions
| diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx index 2f6d1fbaa..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'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 @@ -211,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'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">  @@ -277,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>          );      } @@ -317,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':                  { | 
