From 86726ad1b7ef40ac374b53f9d5902441537df7bd Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 28 Feb 2024 10:38:54 -0500 Subject: changed multirow view to support a maxShown flag so that headerBar doesn't take forever to load with a lot of closed tabs. --- src/client/views/FilterPanel.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src/client/views/FilterPanel.tsx') diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx index 818c81c9a..455801422 100644 --- a/src/client/views/FilterPanel.tsx +++ b/src/client/views/FilterPanel.tsx @@ -121,11 +121,13 @@ export class FilterPanel extends ObservableReactComponent { const valueSet = new Set(childFilters.map(filter => filter.split(Doc.FilterSep)[1])); let rtFields = 0; let subDocs = childDocs; + let gatheredDocs = [] as Doc[]; if (subDocs.length > 0) { let newarray: Doc[] = []; while (subDocs.length > 0) { newarray = []; subDocs.forEach(t => { + gatheredDocs.push(t); const facetVal = t[facetKey]; if (facetVal instanceof RichTextField || typeof facetVal === 'string') rtFields++; facetVal !== undefined && valueSet.add(Field.toString(facetVal as Field)); @@ -135,7 +137,7 @@ export class FilterPanel extends ObservableReactComponent { DocListCast(t[annos ? fieldKey + '_annotations' : fieldKey]).forEach(newdoc => newarray.push(newdoc)); annos && DocListCast(t[fieldKey + '_sidebar']).forEach(newdoc => newarray.push(newdoc)); }); - subDocs = newarray; + subDocs = newarray.filter(d => !gatheredDocs.includes(d)); } } // } -- cgit v1.2.3-70-g09d2 From 4548ec81efc434c8ee5d89b896b2088e4e61d7a0 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 28 Feb 2024 22:22:20 -0500 Subject: extracted field dropdown menu to a components and then cleaned up collectionTimeView, Documentview titles, and FilterPanel to share it --- src/client/documents/Documents.ts | 18 +- src/client/views/FieldsDropdown.tsx | 120 +++++++++++ src/client/views/FilterPanel.tsx | 239 ++++++--------------- .../views/collections/CollectionTimeView.tsx | 55 +---- src/client/views/global/globalScripts.ts | 2 +- src/client/views/nodes/DocumentView.scss | 1 - src/client/views/nodes/DocumentView.tsx | 67 +++--- 7 files changed, 236 insertions(+), 266 deletions(-) create mode 100644 src/client/views/FieldsDropdown.tsx (limited to 'src/client/views/FilterPanel.tsx') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 9b17901ca..031560886 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -77,6 +77,7 @@ export enum FInfoFieldType { enumeration, date, list, + rtf, } export class FInfo { description: string = ''; @@ -84,7 +85,7 @@ export class FInfo { fieldType?: FInfoFieldType; values?: Field[]; - filterable?: boolean = true; + filterable?: boolean = true; // can be used as a Filter in FilterPanel // format?: string; // format to display values (e.g, decimal places, $, etc) // parse?: ScriptField; // parse a value from a string constructor(d: string, readOnly?: boolean) { @@ -165,9 +166,19 @@ class DTypeInfo extends FInfo { override searchable = () => false; } class DateInfo extends FInfo { + constructor(d: string, filterable?: boolean) { + super(d, true); + this.filterable = filterable; + } fieldType? = FInfoFieldType.date; values?: DateField[] = []; - filterable = true; +} +class RtfInfo extends FInfo { + constructor(d: string, filterable?: boolean) { + super(d, true); + this.filterable = filterable; + } + fieldType? = FInfoFieldType.rtf; } class ListInfo extends FInfo { fieldType? = FInfoFieldType.list; @@ -178,6 +189,7 @@ type NUMt = NumInfo | number; type STRt = StrInfo | string; type LISTt = ListInfo | List; type DOCt = DocInfo | Doc; +type RTFt = RtfInfo | RichTextField; type DIMt = DimInfo | typeof DimUnit.Pixel | typeof DimUnit.Ratio; type PEVt = PEInfo | 'none' | 'all'; type COLLt = CTypeInfo | CollectionViewType; @@ -191,6 +203,7 @@ export class DocumentOptions { z?: NUMt = new NumInfo('whether document is in overlay (1) or not (0)', false, false, [1, 0]); overlayX?: NUMt = new NumInfo('x coordinate of document in a overlay view', false); overlayY?: NUMt = new NumInfo('y coordinate of document in a overlay view', false); + text?: RTFt = new RtfInfo('rich text of a text doc', true); _dimMagnitude?: NUMt = new NumInfo("magnitude of collectionMulti{row,col} element's width or height", false); _dimUnit?: DIMt = new DimInfo("units of collectionMulti{row,col} element's width or height - 'px' or '*' for pixels or relative units"); latitude?: NUMt = new NumInfo('latitude coordinate for map views', false); @@ -470,7 +483,6 @@ export class DocumentOptions { sidebar_type_collection?: string; // collection type of text sidebar data_dashboards?: List; // list of dashboards used in shareddocs; - text?: string; textTransform?: string; letterSpacing?: string; iconTemplate?: string; // name of icon template style diff --git a/src/client/views/FieldsDropdown.tsx b/src/client/views/FieldsDropdown.tsx new file mode 100644 index 000000000..5638d34c6 --- /dev/null +++ b/src/client/views/FieldsDropdown.tsx @@ -0,0 +1,120 @@ +/** + * This creates a dropdown menu that's populated with possible field key names (e.g., author, tags) + * + * The set of field names actually displayed is based on searching the prop 'Document' and its descendants : + * The field list will contain all of the fields within the prop Document and all of its children; + * this list is then pruned down to only include fields that are not marked in Documents.ts to be non-filterable + */ + +import { computed, makeObservable, observable, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import Select from 'react-select'; +import { Doc } from '../../fields/Doc'; +import { DocOptions, FInfo } from '../documents/Documents'; +import { SearchUtil } from '../util/SearchUtil'; +import { SettingsManager } from '../util/SettingsManager'; +import './FilterPanel.scss'; +import { ObservableReactComponent } from './ObservableReactComponent'; + +interface fieldsDropdownProps { + Document: Doc; // show fields for this Doc if set, otherwise for all docs in dashboard + selectFunc: (value: string) => void; + menuClose?: () => void; + placeholder?: string | (() => string); + showPlaceholder?: true; // if true, then input field always shows the placeholder value; otherwise, it shows the current selection + addedFields?: string[]; +} + +@observer +export class FieldsDropdown extends ObservableReactComponent { + @observable _newField = ''; + constructor(props: any) { + super(props); + makeObservable(this); + } + + @computed get allDescendantDocs() { + const allDocs = new Set(); + SearchUtil.foreachRecursiveDoc([this._props.Document], (depth, doc) => allDocs.add(doc)); + return Array.from(allDocs); + } + + @computed get fieldsOfDocuments() { + const keys = new Set(); + this.allDescendantDocs.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('_')) || !Doc.noviceMode) + .sort(); + + Array.from(keys).forEach(key => sortedKeys.splice(sortedKeys.indexOf(key), 1)); + + return [...Array.from(keys), ...sortedKeys]; + } + + render() { + const filteredOptions = ['author', ...(this._newField ? [this._newField] : []), ...(this._props.addedFields ?? []), ...this.fieldsOfDocuments.filter(facet => facet[0] === facet.charAt(0).toUpperCase())]; + + Object.entries(DocOptions) + .filter(opts => opts[1].filterable) + .forEach((pair: [string, FInfo]) => filteredOptions.push(pair[0])); + const options = filteredOptions.sort().map(facet => ({ value: facet, label: facet })); + + console.log(options); + return ( + ({ - ...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()} - //onMenuClose={onClose} - value={null} - closeMenuOnSelect={true} - /> - ); - } - render() { return (
-
{this.fieldsDropdown}
+
+ +
{/* THE FOLLOWING CODE SHOULD BE DEVELOPER FOR BOOLEAN EXPRESSION (AND / OR) */} {/*
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)} + onBlur={undoable(e => Doc.setDocFilter(this.Document, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match'), 'set text filter')} + onKeyDown={e => e.key === 'Enter' && undoable(e => Doc.setDocFilter(this.Document, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match'), 'set text filter')(e)} /> ); case 'checkbox': @@ -399,12 +311,12 @@ export class FilterPanel extends ObservableReactComponent { 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')} + onChange={undoable(e => Doc.setDocFilter(this.Document, facetHeader, fval, e.target.checked ? 'check' : 'remove'), 'set filter')} /> {facetValue}
@@ -414,64 +326,51 @@ export class FilterPanel extends ObservableReactComponent { 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()} /> - ))} -
- )} -
-
-
- +
+ Doc.setDocRangeFilter(this.Document, 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()} /> + ))} +
+ )} +
+
+
); } diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx index 38f6aa3e7..b92edd165 100644 --- a/src/client/views/collections/CollectionTimeView.tsx +++ b/src/client/views/collections/CollectionTimeView.tsx @@ -1,12 +1,10 @@ -import { toUpper } from 'lodash'; import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { emptyFunction, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents } from '../../../Utils'; +import { emptyFunction, returnFalse, returnTrue, setupMoveUpEvents } from '../../../Utils'; import { Doc, Opt, StrListCast } from '../../../fields/Doc'; import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; -import { RichTextField } from '../../../fields/RichTextField'; import { listSpec } from '../../../fields/Schema'; import { ComputedField, ScriptField } from '../../../fields/ScriptField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; @@ -15,7 +13,7 @@ import { DocumentManager } from '../../util/DocumentManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; -import { EditableView } from '../EditableView'; +import { FieldsDropdown } from '../FieldsDropdown'; import { DocumentView } from '../nodes/DocumentView'; import { FocusViewOptions } from '../nodes/FieldView'; import { PresBox } from '../nodes/trails'; @@ -192,51 +190,6 @@ export class CollectionTimeView extends CollectionSubView() { ContextMenu.Instance.addItem({ description: 'Options...', subitems: layoutItems, icon: 'eye' }); }; - @computed get _allFacets() { - const facets = new Set(); - this.childDocs.forEach(child => Object.keys(Doc.GetProto(child)).forEach(key => facets.add(key))); - Doc.AreProtosEqual(this.dataDoc, this.Document) && this.childDocs.forEach(child => Object.keys(child).forEach(key => facets.add(key))); - return Array.from(facets); - } - menuCallback = (x: number, y: number) => { - ContextMenu.Instance.clearItems(); - const keySet: Set = new Set(['tags']); - - this.childLayoutPairs.map(pair => - this._allFacets - .filter(fieldKey => pair.layout[fieldKey] instanceof RichTextField || typeof pair.layout[fieldKey] === 'number' || typeof pair.layout[fieldKey] === 'boolean' || typeof pair.layout[fieldKey] === 'string') - .filter(fieldKey => fieldKey[0] !== '_' && (fieldKey === 'tags' || fieldKey[0] === toUpper(fieldKey)[0])) - .map(fieldKey => keySet.add(fieldKey)) - ); - - const docItems: ContextMenuProps[] = Array.from(keySet).map(fieldKey => - ({ description: ':' + fieldKey, event: () => (this.layoutDoc._pivotField = fieldKey), icon: 'compress-arrows-alt' })); // prettier-ignore - docItems.push({ description: ':default', event: () => (this.layoutDoc._pivotField = undefined), icon: 'compress-arrows-alt' }); - ContextMenu.Instance.addItem({ description: 'Pivot Fields ...', subitems: docItems, icon: 'eye' }); - ContextMenu.Instance.displayMenu(x, y, ':'); - }; - - @computed get pivotKeyUI() { - return ( -
- { - if (value?.length) { - this.layoutDoc._pivotField = value; - return true; - } - return false; - }} - background={'#f1efeb'} // this._props.headingObject ? this._props.headingObject.color : "#f1efeb"; - contents={':' + StrCast(this.layoutDoc._pivotField)} - showMenuOnLoad={true} - display={'inline'} - menuCallback={this.menuCallback} - /> -
- ); - } render() { let nonNumbers = 0; @@ -263,7 +216,6 @@ export class CollectionTimeView extends CollectionSubView() { return (
- {this.pivotKeyUI} {this.contents} {!this._props.isSelected() || !doTimeline ? null : ( <> @@ -272,6 +224,9 @@ export class CollectionTimeView extends CollectionSubView() {
)} +
+ (this.layoutDoc._pivotField = fieldKey)} placeholder={StrCast(this.layoutDoc._pivotField)} /> +
); } diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index 51672513b..e73ab25b5 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -78,7 +78,7 @@ ScriptingGlobals.add(function setHeaderColor(color?: string, checkResult?: boole } if (SelectionManager.Views.length) { SelectionManager.Docs.forEach(doc => { - Doc.GetProto(doc).layout_headingColor = color; + doc[DocData].layout_headingColor = color === 'transparent' ? undefined : color; doc.layout_showTitle = color === 'transparent' ? undefined : StrCast(doc.layout_showTitle, 'title'); }); } else { diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index c4dab16fb..5421c1b50 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -180,7 +180,6 @@ .documentView-titleWrapper, .documentView-titleWrapper-hover { - overflow: hidden; color: $black; transform-origin: top left; top: 0; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index abb1aa59f..b0e8cf6ff 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -48,6 +48,7 @@ import { KeyValueBox } from './KeyValueBox'; import { LinkAnchorBox } from './LinkAnchorBox'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { PresEffect, PresEffectDirection } from './trails'; +import { FieldsDropdown } from '../FieldsDropdown'; interface Window { MediaRecorder: MediaRecorder; } @@ -784,28 +785,25 @@ export class DocumentViewInternal extends DocComponent, props: Opt, property: string) => this._props?.styleProvider?.(doc, props, property + ':caption'); - fieldsDropdown = (reqdFields: string[], dropdownWidth: number, placeholder: string, onChange: (val: string | number) => void, onClose: () => void) => { - const filteredFields = Object.entries(DocOptions).reduce((set, [field, opts]) => (opts.filterable ? set.add(field) : set), new Set(reqdFields)); + fieldsDropdown = (placeholder: string) => { return ( -
-
r && (this._titleDropDownInnerWidth = Number(getComputedStyle(r).width.replace('px', ''))))} - onPointerDown={action(e => (this._changingTitleField = true))} - style={{ width: 'max-content', transformOrigin: 'left', transform: `scale(${this.titleHeight / 30 /* height of Dropdown */})` }}> - !isOpen && (this._changingTitleField = false))} - selectedVal={placeholder} - setSelectedVal={onChange} - color={SettingsManager.userColor} - background={SettingsManager.userVariantColor} - type={Type.TERT} - closeOnSelect={true} - dropdownType={DropdownType.SELECT} - items={Array.from(filteredFields).map(facet => ({ val: facet, text: facet }))} - width={100} - fillWidth - /> -
+
r && (this._titleDropDownInnerWidth = Number(getComputedStyle(r).width.replace('px', ''))))} + onPointerDown={action(e => (this._changingTitleField = true))} + style={{ width: 'max-content', background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor, transformOrigin: 'left', transform: `scale(${this.titleHeight / 30 /* height of Dropdown */})` }}> + { + if (this.layoutDoc.layout_showTitle) { + this.layoutDoc._layout_showTitle = field; + } else if (!this._props.layout_showTitle) { + Doc.UserDoc().layout_showTitle = field; + } + this._changingTitleField = false; + })} + menuClose={action(() => (this._changingTitleField = false))} + />
); }; @@ -839,22 +837,7 @@ export class DocumentViewInternal extends DocComponent - {!dropdownWidth - ? null - : this.fieldsDropdown( - [StrCast(this.layoutDoc.layout_showTitle)], - dropdownWidth, - StrCast(this.layoutDoc.layout_showTitle).split(':')[0], - action((field: string | number) => { - if (this.layoutDoc.layout_showTitle) { - this.layoutDoc._layout_showTitle = field; - } else if (!this._props.layout_showTitle) { - Doc.UserDoc().layout_showTitle = field; - } - this._changingTitleField = false; - }), - action(() => (this._changingTitleField = false)) - )} + {!dropdownWidth ? null :
{this.fieldsDropdown(showTitle)}
}
targetDoc[field.trim()]?.toString()) - .join(' \\ ')} + contents={ + showTitle + .split(';') + .map(field => targetDoc[field.trim()]?.toString()) + .join(' \\ ') || '-unset-' + } display="block" oneLine={true} fontSize={(this.titleHeight / 15) * 10} -- cgit v1.2.3-70-g09d2