aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/views/collections/CollectionSubView.tsx40
-rw-r--r--src/client/views/collections/CollectionTimeView.scss51
-rw-r--r--src/client/views/collections/CollectionTimeView.tsx244
-rw-r--r--src/client/views/collections/CollectionView.scss54
-rw-r--r--src/client/views/collections/CollectionView.tsx175
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx40
6 files changed, 324 insertions, 280 deletions
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 97177855f..a5480d567 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -112,12 +112,48 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
get childDocList() {
return Cast(this.dataField, listSpec(Doc));
}
- get childDocs() {
+ @computed get childDocs() {
+ const docFilters = Cast(this.props.Document._docFilters, listSpec("string"), []);
+ const docRangeFilters = Cast(this.props.Document._docRangeFilters, listSpec("string"), []);
+ const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields
+ for (let i = 0; i < docFilters.length; i += 3) {
+ const [key, value, modifiers] = docFilters.slice(i, i + 3);
+ if (!filterFacets[key]) {
+ filterFacets[key] = {};
+ }
+ filterFacets[key][value] = modifiers;
+ }
+
const dfield = this.dataField;
const rawdocs = (dfield instanceof Doc) ? [dfield] : Cast(dfield, listSpec(Doc), this.props.Document.expandedTemplate && !this.props.annotationsKey ? [Cast(this.props.Document.expandedTemplate, Doc, null)] : []);
const docs = rawdocs.filter(d => !(d instanceof Promise)).map(d => d as Doc);
const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
- return viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
+ const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
+
+ const filteredDocs = docFilters.length ? childDocs.filter(d => {
+ for (const facetKey of Object.keys(filterFacets)) {
+ const facet = filterFacets[facetKey];
+ const satisfiesFacet = Object.keys(facet).some(value =>
+ (facet[value] === "x") !== Doc.matchFieldValue(d, facetKey, value));
+ if (!satisfiesFacet) {
+ return false;
+ }
+ }
+ return true;
+ }) : childDocs;
+ const rangeFilteredDocs = filteredDocs.filter(d => {
+ for (let i = 0; i < docRangeFilters.length; i += 3) {
+ const key = docRangeFilters[i];
+ const min = Number(docRangeFilters[i + 1]);
+ const max = Number(docRangeFilters[i + 2]);
+ const val = Cast(d[key], "number", null);
+ if (val !== undefined && (val < min || val > max)) {
+ return false;
+ }
+ }
+ return true;
+ });
+ return rangeFilteredDocs;
}
@action
diff --git a/src/client/views/collections/CollectionTimeView.scss b/src/client/views/collections/CollectionTimeView.scss
index 310668ad2..be745000e 100644
--- a/src/client/views/collections/CollectionTimeView.scss
+++ b/src/client/views/collections/CollectionTimeView.scss
@@ -72,62 +72,11 @@
}
}
- .collectionTimeView-treeView {
- display: flex;
- flex-direction: column;
- width: 200px;
- height: 100%;
-
- .collectionTimeView-addfacet {
- display: inline-block;
- width: 200px;
- height: 30px;
- background: darkGray;
- text-align: center;
-
- .collectionTimeView-button {
- align-items: center;
- display: flex;
- width: 100%;
- height: 100%;
-
- .collectionTimeView-span {
- margin: auto;
- }
- }
-
- >div,
- >div>div {
- width: 100%;
- height: 100%;
- text-align: center;
- }
- }
-
- .collectionTimeView-tree {
- display: inline-block;
- width: 100%;
- height: calc(100% - 30px);
- }
- }
-
.collectionTimeView-innards {
display: inline-block;
width: calc(100% - 200px);
height: 100%;
}
-
- .collectionTimeView-dragger {
- background-color: lightgray;
- height: 40px;
- width: 20px;
- position: absolute;
- border-radius: 10px;
- top: 55%;
- border: 1px black solid;
- z-index: 2;
- left: -10px;
- }
}
.collectionTimeView-pivot {
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 6d3b9b7f1..69922d6d9 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -1,16 +1,12 @@
-import { faEdit } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, observable, runInAction, trace } from "mobx";
+import { action, computed, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DocListCast, Field, WidthSym, HeightSym } from "../../../new_fields/Doc";
+import { Doc } from "../../../new_fields/Doc";
import { List } from "../../../new_fields/List";
import { ObjectField } from "../../../new_fields/ObjectField";
import { RichTextField } from "../../../new_fields/RichTextField";
-import { listSpec } from "../../../new_fields/Schema";
import { ComputedField, ScriptField } from "../../../new_fields/ScriptField";
-import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { Docs } from "../../documents/Documents";
-import { DocumentType } from "../../documents/DocumentTypes";
+import { NumCast, StrCast } from "../../../new_fields/Types";
+import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../../Utils";
import { Scripting } from "../../util/Scripting";
import { ContextMenu } from "../ContextMenu";
import { ContextMenuProps } from "../ContextMenuItem";
@@ -19,145 +15,33 @@ import { ViewDefBounds } from "./collectionFreeForm/CollectionFreeFormLayoutEngi
import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
import { CollectionSubView } from "./CollectionSubView";
import "./CollectionTimeView.scss";
-import { CollectionTreeView } from "./CollectionTreeView";
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
import React = require("react");
-import { setupMoveUpEvents, returnFalse, emptyFunction } from "../../../Utils";
@observer
export class CollectionTimeView extends CollectionSubView(doc => doc) {
_changing = false;
@observable _layoutEngine = "pivot";
- @observable _facetWidth = 0;
@observable _collapsed: boolean = false;
componentWillUnmount() {
this.props.Document.onChildClick = undefined;
}
componentDidMount() {
this.props.Document._freezeOnDrop = true;
- if (!this.props.Document._facetCollection) {
- const scriptText = "setDocFilter(containingTreeView.target, heading, this.title, checked)";
- const facetCollection = Docs.Create.TreeDocument([], { title: "facetFilters", _yMargin: 0, treeViewHideTitle: true, treeViewHideHeaderFields: true });
- facetCollection.target = this.props.Document;
- facetCollection.onCheckedClick = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name });
- this.props.Document.excludeFields = new List<string>(["_facetCollection", "_docFilters"]);
-
- this.props.Document._facetCollection = facetCollection;
- this.props.Document._fitToBox = true;
- }
const childDetailed = this.props.Document.childDetailed; // bcz: needs to be here to make sure the childDetailed layout template has been loaded when the first item is clicked;
const childText = "const alias = getAlias(this); Doc.ApplyTemplateTo(containingCollection.childDetailed, alias, 'layout_detailView'); alias.dropAction='alias'; alias.removeDropProperties=new List<string>(['dropAction']); useRightSplit(alias, shiftKey); ";
this.props.Document.onChildClick = ScriptField.MakeScript(childText, { this: Doc.name, heading: "string", containingCollection: Doc.name, shiftKey: "boolean" });
-
+ this.props.Document._fitToBox = true;
if (!this.props.Document.onViewDefClick) {
this.props.Document.onViewDefDivClick = ScriptField.MakeScript("pivotColumnClick(this,payload)", { payload: "any" });
}
}
- bodyPanelWidth = () => this.props.PanelWidth() - this._facetWidth;
- getTransform = () => this.props.ScreenToLocalTransform().translate(-this._facetWidth, 0);
layoutEngine = () => this._layoutEngine;
- facetWidth = () => this._facetWidth;
toggleVisibility = action(() => this._collapsed = !this._collapsed);
- @computed get _allFacets() {
- const facets = new Set<string>();
- this.childDocs.forEach(child => Object.keys(Doc.GetProto(child)).forEach(key => facets.add(key)));
- Doc.AreProtosEqual(this.dataDoc, this.props.Document) && this.childDocs.forEach(child => Object.keys(child).forEach(key => facets.add(key)));
- return Array.from(facets);
- }
-
- /**
- * Responds to clicking the check box in the flyout menu
- */
- facetClick = (facetHeader: string) => {
- const facetCollection = this.props.Document._facetCollection;
- if (facetCollection instanceof Doc) {
- const found = DocListCast(facetCollection.data).findIndex(doc => doc.title === facetHeader);
- if (found !== -1) {
- (facetCollection.data as List<Doc>).splice(found, 1);
- const docFilter = Cast(this.props.Document._docFilters, listSpec("string"));
- if (docFilter) {
- let index: number;
- while ((index = docFilter.findIndex(item => item === facetHeader)) !== -1) {
- docFilter.splice(index, 3);
- }
- }
- const docRangeFilters = Cast(this.props.Document._docRangeFilters, listSpec("string"));
- if (docRangeFilters) {
- let index: number;
- while ((index = docRangeFilters.findIndex(item => item === facetHeader)) !== -1) {
- docRangeFilters.splice(index, 3);
- }
- }
- } else {
- const allCollectionDocs = DocListCast(this.dataDoc[this.props.fieldKey]);
- const facetValues = Array.from(allCollectionDocs.reduce((set, child) =>
- set.add(Field.toString(child[facetHeader] as Field)), new Set<string>()));
-
- let nonNumbers = 0;
- let minVal = Number.MAX_VALUE, maxVal = -Number.MAX_VALUE;
- facetValues.map(val => {
- const num = Number(val);
- if (Number.isNaN(num)) {
- nonNumbers++;
- } else {
- minVal = Math.min(num, minVal);
- maxVal = Math.max(num, maxVal);
- }
- });
- if (nonNumbers / allCollectionDocs.length < .1) {
- const ranged = Doc.readDocRangeFilter(this.props.Document, facetHeader);
- const newFacet = Docs.Create.SliderDocument({ title: facetHeader });
- Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox
- newFacet.treeViewExpandedView = "layout";
- newFacet.treeViewOpen = true;
- newFacet._sliderMin = ranged === undefined ? minVal : ranged[0];
- newFacet._sliderMax = ranged === undefined ? maxVal : ranged[1];
- newFacet._sliderMinThumb = minVal;
- newFacet._sliderMaxThumb = maxVal;
- newFacet.target = this.props.Document;
- const scriptText = `setDocFilterRange(this.target, "${facetHeader}", range)`;
- newFacet.onThumbChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, range: "number" });
-
- Doc.AddDocToList(facetCollection, "data", newFacet);
- } else {
- const newFacet = Docs.Create.TreeDocument([], { title: facetHeader, treeViewOpen: true, isFacetFilter: true });
- const capturedVariables = { layoutDoc: this.props.Document, dataDoc: this.dataDoc };
- const params = { layoutDoc: Doc.name, dataDoc: Doc.name, };
- newFacet.data = ComputedField.MakeFunction(`readFacetData(layoutDoc, dataDoc, "${this.props.fieldKey}", "${facetHeader}")`, params, capturedVariables);
- Doc.AddDocToList(facetCollection, "data", newFacet);
- }
- }
- }
- }
-
- menuCallback = (x: number, y: number) => {
- ContextMenu.Instance.clearItems();
- const docItems: ContextMenuProps[] = [];
- const keySet: Set<string> = new Set();
-
- this.childLayoutPairs.map(pair => this._allFacets.filter(fieldKey =>
- pair.layout[fieldKey] instanceof RichTextField ||
- typeof (pair.layout[fieldKey]) === "number" ||
- typeof (pair.layout[fieldKey]) === "string").map(fieldKey => keySet.add(fieldKey)));
- Array.from(keySet).map(fieldKey =>
- docItems.push({ description: ":" + fieldKey, event: () => this.props.Document._pivotField = fieldKey, icon: "compress-arrows-alt" }));
- docItems.push({ description: ":(null)", event: () => this.props.Document._pivotField = undefined, icon: "compress-arrows-alt" });
- ContextMenu.Instance.addItem({ description: "Pivot Fields ...", subitems: docItems, icon: "eye" });
- const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(x, y);
- ContextMenu.Instance.displayMenu(x, y, ":");
- }
-
- onPointerDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => {
- this._facetWidth = Math.max(this.props.ScreenToLocalTransform().transformPoint(e.clientX,0)[0], 0);
- return false;
- }), returnFalse, () => this._facetWidth = this._facetWidth < 15 ? 200 : 0);
- }
-
onMinDown = (e: React.PointerEvent) => {
setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => {
const minReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMinReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMin"], 0));
@@ -188,34 +72,8 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
}
@computed get contents() {
- return <div className="collectionTimeView-innards" key="timeline" style={{ width: this.bodyPanelWidth() }}>
- <CollectionFreeFormView {...this.props} layoutEngine={this.layoutEngine} ScreenToLocalTransform={this.getTransform} PanelWidth={this.bodyPanelWidth} />
- </div>;
- }
- @computed get filterView() {
- trace();
- const facetCollection = Cast(this.props.Document?._facetCollection, Doc, null);
- const flyout = (
- <div className="collectionTimeView-flyout" style={{ width: `${this._facetWidth}`, height: this.props.PanelHeight() - 30 }} onWheel={e => e.stopPropagation()}>
- {this._allFacets.map(facet => <label className="collectionTimeView-flyout-item" key={`${facet}`} onClick={e => this.facetClick(facet)}>
- <input type="checkbox" onChange={e => { }} checked={DocListCast((this.props.Document._facetCollection as Doc)?.data).some(d => d.title === facet)} />
- <span className="checkmark" />
- {facet}
- </label>)}
- </div>
- );
- return <div className="collectionTimeView-treeView" style={{ width: `${this._facetWidth}px`, overflow: this._facetWidth < 15 ? "hidden" : undefined }}>
- <div className="collectionTimeView-addFacet" style={{ width: `${this._facetWidth}px` }} onPointerDown={e => e.stopPropagation()}>
- <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}>
- <div className="collectionTimeView-button">
- <span className="collectionTimeView-span">Facet Filters</span>
- <FontAwesomeIcon icon={faEdit} size={"lg"} />
- </div>
- </Flyout>
- </div>
- <div className="collectionTimeView-tree" key="tree">
- <CollectionTreeView {...this.props} PanelWidth={this.facetWidth} DataDoc={undefined} Document={facetCollection} />
- </div>
+ return <div className="collectionTimeView-innards" key="timeline" style={{ width: "100%" }}>
+ <CollectionFreeFormView {...this.props} layoutEngine={this.layoutEngine} />
</div>;
}
@@ -234,8 +92,30 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
ContextMenu.Instance.addItem({ description: "Pivot/Time Options ...", subitems: layoutItems, icon: "eye" });
}
+ @computed get _allFacets() {
+ const facets = new Set<string>();
+ this.childDocs.forEach(child => Object.keys(Doc.GetProto(child)).forEach(key => facets.add(key)));
+ Doc.AreProtosEqual(this.dataDoc, this.props.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 docItems: ContextMenuProps[] = [];
+ const keySet: Set<string> = new Set();
- render() {
+ this.childLayoutPairs.map(pair => this._allFacets.filter(fieldKey =>
+ pair.layout[fieldKey] instanceof RichTextField ||
+ typeof (pair.layout[fieldKey]) === "number" ||
+ typeof (pair.layout[fieldKey]) === "string").map(fieldKey => keySet.add(fieldKey)));
+ Array.from(keySet).map(fieldKey =>
+ docItems.push({ description: ":" + fieldKey, event: () => this.props.Document._pivotField = fieldKey, icon: "compress-arrows-alt" }));
+ docItems.push({ description: ":(null)", event: () => this.props.Document._pivotField = undefined, icon: "compress-arrows-alt" });
+ ContextMenu.Instance.addItem({ description: "Pivot Fields ...", subitems: docItems, icon: "eye" });
+ const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(x, y);
+ ContextMenu.Instance.displayMenu(x, y, ":");
+ }
+
+ @computed get pivotKeyUI() {
const newEditableViewProps = {
GetValue: () => "",
SetValue: (value: any) => {
@@ -250,7 +130,26 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
toggle: this.toggleVisibility,
color: "#f1efeb" // this.props.headingObject ? this.props.headingObject.color : "#f1efeb";
};
+ return <div className={"pivotKeyEntry"}>
+ <button className="collectionTimeView-backBtn"
+ onClick={action(() => {
+ let prevFilterIndex = NumCast(this.props.Document._prevFilterIndex);
+ if (prevFilterIndex > 0) {
+ prevFilterIndex--;
+ this.props.Document._docFilters = ObjectField.MakeCopy(this.props.Document["_prevDocFilter" + prevFilterIndex] as ObjectField);
+ this.props.Document._docRangeFilters = ObjectField.MakeCopy(this.props.Document["_prevDocRangeFilters" + prevFilterIndex] as ObjectField);
+ this.props.Document._prevFilterIndex = prevFilterIndex;
+ } else {
+ this.props.Document._docFilters = new List([]);
+ }
+ })}>
+ back
+ </button>
+ <EditableView {...newEditableViewProps} display={"inline"} menuCallback={this.menuCallback} />
+ </div>
+ }
+ render() {
let nonNumbers = 0;
this.childDocs.map(doc => {
const num = NumCast(doc[StrCast(this.props.Document._pivotField)], Number(StrCast(doc[StrCast(this.props.Document._pivotField)])));
@@ -270,41 +169,16 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
}
}
-
- const facetCollection = Cast(this.props.Document?._facetCollection, Doc, null);
- return !facetCollection ? (null) :
- <div className={"collectionTimeView" + (doTimeline ? "" : "-pivot")} onContextMenu={this.specificMenu}
- style={{ height: `calc(100% - ${this.props.Document._chromeStatus === "enabled" ? 51 : 0}px)` }}>
- <div className={"pivotKeyEntry"}>
- <button className="collectionTimeView-backBtn"
- onClick={action(() => {
- let prevFilterIndex = NumCast(this.props.Document._prevFilterIndex);
- if (prevFilterIndex > 0) {
- prevFilterIndex--;
- this.props.Document._docFilters = ObjectField.MakeCopy(this.props.Document["_prevDocFilter" + prevFilterIndex] as ObjectField);
- this.props.Document._docRangeFilters = ObjectField.MakeCopy(this.props.Document["_prevDocRangeFilters" + prevFilterIndex] as ObjectField);
- this.props.Document._prevFilterIndex = prevFilterIndex;
- } else {
- this.props.Document._docFilters = new List([]);
- }
- })}>
- back
- </button>
- <EditableView {...newEditableViewProps} display={"inline"} menuCallback={this.menuCallback} />
- </div>
- {!this.props.isSelected() || this.props.PanelHeight() < 100 ? (null) :
- <div className="collectionTimeView-dragger" key="dragger" onPointerDown={this.onPointerDown} style={{ transform: `translate(${this._facetWidth}px, 0px)` }} >
- <span title="library View Dragger" style={{ width: "5px", position: "absolute", top: "0" }} />
- </div>
- }
- {this.filterView}
- {this.contents}
- {!this.props.isSelected() || !doTimeline ? (null) : <>
- <div className="collectionTimeView-thumb-min collectionTimeView-thumb" key="min" onPointerDown={this.onMinDown} />
- <div className="collectionTimeView-thumb-max collectionTimeView-thumb" key="mid" onPointerDown={this.onMaxDown} />
- <div className="collectionTimeView-thumb-mid collectionTimeView-thumb" key="max" onPointerDown={this.onMidDown} />
- </>}
- </div>;
+ return <div className={"collectionTimeView" + (doTimeline ? "" : "-pivot")} onContextMenu={this.specificMenu}
+ style={{ height: `calc(100% - ${this.props.Document._chromeStatus === "enabled" ? 51 : 0}px)` }}>
+ {this.pivotKeyUI}
+ {this.contents}
+ {!this.props.isSelected() || !doTimeline ? (null) : <>
+ <div className="collectionTimeView-thumb-min collectionTimeView-thumb" key="min" onPointerDown={this.onMinDown} />
+ <div className="collectionTimeView-thumb-max collectionTimeView-thumb" key="mid" onPointerDown={this.onMaxDown} />
+ <div className="collectionTimeView-thumb-mid collectionTimeView-thumb" key="max" onPointerDown={this.onMidDown} />
+ </>}
+ </div>;
}
}
diff --git a/src/client/views/collections/CollectionView.scss b/src/client/views/collections/CollectionView.scss
index 1c46081a1..0246abccc 100644
--- a/src/client/views/collections/CollectionView.scss
+++ b/src/client/views/collections/CollectionView.scss
@@ -10,6 +10,60 @@
width: 100%;
height: 100%;
overflow: hidden; // bcz: used to be 'auto' which would create scrollbars when there's a floating doc that's not visible. not sure if that's better, but the scrollbars are annoying...
+
+
+ .collectionTimeView-dragger {
+ background-color: lightgray;
+ height: 40px;
+ width: 20px;
+ position: absolute;
+ border-radius: 10px;
+ top: 55%;
+ border: 1px black solid;
+ z-index: 2;
+ left: -10px;
+ }
+ .collectionTimeView-treeView {
+ display: flex;
+ flex-direction: column;
+ width: 200px;
+ height: 100%;
+ position: absolute;
+ left: 0;
+ top: 0;
+
+ .collectionTimeView-addfacet {
+ display: inline-block;
+ width: 200px;
+ height: 30px;
+ background: darkGray;
+ text-align: left;
+
+ .collectionTimeView-button {
+ align-items: center;
+ display: flex;
+ width: 100%;
+ height: 100%;
+
+ .collectionTimeView-span {
+ margin: auto;
+ }
+ }
+
+ >div,
+ >div>div {
+ width: 100%;
+ height: 100%;
+ text-align: center;
+ }
+ }
+
+ .collectionTimeView-tree {
+ display: inline-block;
+ width: 100%;
+ height: calc(100% - 30px);
+ }
+ }
}
#google-tags {
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 7683e1550..a1f173746 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -1,18 +1,19 @@
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faEye } from '@fortawesome/free-regular-svg-icons';
+import { faEye, faEdit } from '@fortawesome/free-regular-svg-icons';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faColumns, faCopy, faEllipsisV, faFingerprint, faImage, faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons';
-import { action, observable } from 'mobx';
+import { action, observable, computed } from 'mobx';
import { observer } from "mobx-react";
import * as React from 'react';
import Lightbox from 'react-image-lightbox-with-rotate';
import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app
import { DateField } from '../../../new_fields/DateField';
-import { DataSym, Doc, DocListCast } from '../../../new_fields/Doc';
+import { DataSym, Doc, DocListCast, Field } from '../../../new_fields/Doc';
import { List } from '../../../new_fields/List';
import { BoolCast, Cast, NumCast, StrCast } from '../../../new_fields/Types';
import { ImageField } from '../../../new_fields/URLField';
import { TraceMobx } from '../../../new_fields/util';
-import { Utils } from '../../../Utils';
+import { Utils, setupMoveUpEvents, returnFalse } from '../../../Utils';
import { DocumentType } from '../../documents/DocumentTypes';
import { DocumentManager } from '../../util/DocumentManager';
import { ImageUtils } from '../../util/Import & Export/ImageUtils';
@@ -38,6 +39,12 @@ import './CollectionView.scss';
import { CollectionViewBaseChrome } from './CollectionViewChromes';
import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
import { Id } from '../../../new_fields/FieldSymbols';
+import { listSpec } from '../../../new_fields/Schema';
+import { Docs } from '../../documents/Documents';
+import { ScriptField, ComputedField } from '../../../new_fields/ScriptField';
+const higflyout = require("@hig/flyout");
+export const { anchorPoints } = higflyout;
+export const Flyout = higflyout.default;
export const COLLECTION_BORDER_WIDTH = 2;
const path = require('path');
library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any, faCopy);
@@ -84,6 +91,7 @@ export interface CollectionRenderProps {
moveDocument: (document: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean;
active: () => boolean;
whenActiveChanged: (isActive: boolean) => void;
+ PanelWidth: () => number;
}
@observer
@@ -268,6 +276,158 @@ export class CollectionView extends Touchable<FieldViewProps> {
onMovePrevRequest={action(() => this._curLightboxImg = (this._curLightboxImg + images.length - 1) % images.length)}
onMoveNextRequest={action(() => this._curLightboxImg = (this._curLightboxImg + 1) % images.length)} />);
}
+ @observable _facetWidth = 0;
+
+ bodyPanelWidth = () => this.props.PanelWidth() - this._facetWidth;
+ getTransform = () => this.props.ScreenToLocalTransform().translate(-this._facetWidth, 0);
+ facetWidth = () => this._facetWidth;
+
+ @computed get dataDoc() {
+ return (this.props.DataDoc && this.props.Document.isTemplateForField ? Doc.GetProto(this.props.DataDoc) :
+ this.props.Document.resolvedDataDoc ? this.props.Document : Doc.GetProto(this.props.Document)); // if the layout document has a resolvedDataDoc, then we don't want to get its parent which would be the unexpanded template
+ }
+ // The data field for rendering this collection will be on the this.props.Document unless we're rendering a template in which case we try to use props.DataDoc.
+ // When a document has a DataDoc but it's not a template, then it contains its own rendering data, but needs to pass the DataDoc through
+ // to its children which may be templates.
+ // If 'annotationField' is specified, then all children exist on that field of the extension document, otherwise, they exist directly on the data document under 'fieldKey'
+ @computed get dataField() {
+ return this.dataDoc[this.props.fieldKey];
+ }
+
+ get childLayoutPairs(): { layout: Doc; data: Doc; }[] {
+ const { Document, DataDoc } = this.props;
+ const validPairs = this.childDocs.map(doc => Doc.GetLayoutDataDocPair(Document, DataDoc, doc)).filter(pair => pair.layout);
+ return validPairs.map(({ data, layout }) => ({ data, layout: layout! })); // this mapping is a bit of a hack to coerce types
+ }
+ get childDocList() {
+ return Cast(this.dataField, listSpec(Doc));
+ }
+ get childDocs() {
+ const dfield = this.dataField;
+ const rawdocs = (dfield instanceof Doc) ? [dfield] : Cast(dfield, listSpec(Doc), this.props.Document.expandedTemplate ? [Cast(this.props.Document.expandedTemplate, Doc, null)] : []);
+ const docs = rawdocs.filter(d => !(d instanceof Promise)).map(d => d as Doc);
+ const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
+ return viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
+ }
+ @computed get _allFacets() {
+ const facets = new Set<string>();
+ this.childDocs.forEach(child => Object.keys(Doc.GetProto(child)).forEach(key => facets.add(key)));
+ Doc.AreProtosEqual(this.dataDoc, this.props.Document) && this.childDocs.forEach(child => Object.keys(child).forEach(key => facets.add(key)));
+ return Array.from(facets);
+ }
+
+ /**
+ * Responds to clicking the check box in the flyout menu
+ */
+ facetClick = (facetHeader: string) => {
+ const facetCollection = this.props.Document._facetCollection;
+ if (facetCollection instanceof Doc) {
+ const found = DocListCast(facetCollection.data).findIndex(doc => doc.title === facetHeader);
+ if (found !== -1) {
+ (facetCollection.data as List<Doc>).splice(found, 1);
+ const docFilter = Cast(this.props.Document._docFilters, listSpec("string"));
+ if (docFilter) {
+ let index: number;
+ while ((index = docFilter.findIndex(item => item === facetHeader)) !== -1) {
+ docFilter.splice(index, 3);
+ }
+ }
+ const docRangeFilters = Cast(this.props.Document._docRangeFilters, listSpec("string"));
+ if (docRangeFilters) {
+ let index: number;
+ while ((index = docRangeFilters.findIndex(item => item === facetHeader)) !== -1) {
+ docRangeFilters.splice(index, 3);
+ }
+ }
+ } else {
+ const allCollectionDocs = DocListCast(this.dataDoc[this.props.fieldKey]);
+ const facetValues = Array.from(allCollectionDocs.reduce((set, child) =>
+ set.add(Field.toString(child[facetHeader] as Field)), new Set<string>()));
+
+ let nonNumbers = 0;
+ let minVal = Number.MAX_VALUE, maxVal = -Number.MAX_VALUE;
+ facetValues.map(val => {
+ const num = Number(val);
+ if (Number.isNaN(num)) {
+ nonNumbers++;
+ } else {
+ minVal = Math.min(num, minVal);
+ maxVal = Math.max(num, maxVal);
+ }
+ });
+ if (nonNumbers / allCollectionDocs.length < .1) {
+ const ranged = Doc.readDocRangeFilter(this.props.Document, facetHeader);
+ const newFacet = Docs.Create.SliderDocument({ title: facetHeader });
+ Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox
+ newFacet.treeViewExpandedView = "layout";
+ newFacet.treeViewOpen = true;
+ newFacet._sliderMin = ranged === undefined ? minVal : ranged[0];
+ newFacet._sliderMax = ranged === undefined ? maxVal : ranged[1];
+ newFacet._sliderMinThumb = minVal;
+ newFacet._sliderMaxThumb = maxVal;
+ newFacet.target = this.props.Document;
+ const scriptText = `setDocFilterRange(this.target, "${facetHeader}", range)`;
+ newFacet.onThumbChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, range: "number" });
+
+ Doc.AddDocToList(facetCollection, "data", newFacet);
+ } else {
+ const newFacet = Docs.Create.TreeDocument([], { title: facetHeader, treeViewOpen: true, isFacetFilter: true });
+ const capturedVariables = { layoutDoc: this.props.Document, dataDoc: this.dataDoc };
+ const params = { layoutDoc: Doc.name, dataDoc: Doc.name, };
+ newFacet.data = ComputedField.MakeFunction(`readFacetData(layoutDoc, dataDoc, "${this.props.fieldKey}", "${facetHeader}")`, params, capturedVariables);
+ Doc.AddDocToList(facetCollection, "data", newFacet);
+ }
+ }
+ }
+ }
+
+
+ onPointerDown = (e: React.PointerEvent) => {
+ setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => {
+ this._facetWidth = Math.max(this.props.ScreenToLocalTransform().transformPoint(e.clientX, 0)[0], 0);
+ return false;
+ }), returnFalse, action(() => this._facetWidth = this._facetWidth < 15 ? 200 : 0));
+ }
+ @computed get filterView() {
+ const facetCollection = Cast(this.props.Document?._facetCollection, Doc);
+ if (this._facetWidth && facetCollection === undefined) setTimeout(() => {
+ const scriptText = "setDocFilter(containingTreeView.target, heading, this.title, checked)";
+ const facetCollection = Docs.Create.TreeDocument([], { title: "facetFilters", _yMargin: 0, treeViewHideTitle: true, treeViewHideHeaderFields: true });
+ facetCollection.target = this.props.Document;
+ facetCollection.onCheckedClick = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name });
+ this.props.Document.excludeFields = new List<string>(["_facetCollection", "_docFilters"]);
+
+ this.props.Document._facetCollection = facetCollection;
+ }, 0);
+ const flyout = (
+ <div className="collectionTimeView-flyout" style={{ width: `${this._facetWidth}`, height: this.props.PanelHeight() - 30 }} onWheel={e => e.stopPropagation()}>
+ {this._allFacets.map(facet => <label className="collectionTimeView-flyout-item" key={`${facet}`} onClick={e => this.facetClick(facet)}>
+ <input type="checkbox" onChange={e => { }} checked={DocListCast((this.props.Document._facetCollection as Doc)?.data).some(d => d.title === facet)} />
+ <span className="checkmark" />
+ {facet}
+ </label>)}
+ </div>
+ );
+ return !facetCollection ? (null) :
+ <div className="collectionTimeView-treeView" style={{ width: `${this._facetWidth}px`, overflow: this._facetWidth < 15 ? "hidden" : undefined }}>
+ <div className="collectionTimeView-addFacet" style={{ width: `${this._facetWidth}px` }} onPointerDown={e => e.stopPropagation()}>
+ <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}>
+ <div className="collectionTimeView-button">
+ <span className="collectionTimeView-span">Facet Filters</span>
+ <FontAwesomeIcon icon={faEdit} size={"lg"} />
+ </div>
+ </Flyout>
+ </div>
+ <div className="collectionTimeView-tree" key="tree">
+ <CollectionTreeView {...this.props} CollectionView={this} annotationsKey={""} PanelWidth={this.facetWidth}
+ DataDoc={undefined} Document={facetCollection}
+ moveDocument={(doc: Doc) => false}
+ removeDocument={(doc: Doc) => false}
+ addDocument={(doc: Doc) => false} />
+ </div>
+ </div>;
+ }
+
render() {
TraceMobx();
const props: CollectionRenderProps = {
@@ -276,6 +436,7 @@ export class CollectionView extends Touchable<FieldViewProps> {
moveDocument: this.moveDocument,
active: this.active,
whenActiveChanged: this.whenActiveChanged,
+ PanelWidth: this.bodyPanelWidth
};
return (<div className={"collectionView"}
style={{
@@ -292,6 +453,12 @@ export class CollectionView extends Touchable<FieldViewProps> {
Utils.CorsProxy(Cast(d.data, ImageField)!.url.href) : Cast(d.data, ImageField)!.url.href
:
""))}
+ {!this.props.isSelected() || this.props.PanelHeight() < 100 ? (null) :
+ <div className="collectionTimeView-dragger" key="dragger" onPointerDown={this.onPointerDown} style={{ transform: `translate(${this._facetWidth}px, 0px)` }} >
+ <span title="library View Dragger" style={{ width: "5px", position: "absolute", top: "0" }} />
+ </div>
+ }
+ {this.filterView}
</div>);
}
} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index da44d0d59..a0dd4f2de 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -903,12 +903,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}.bind(this));
doTimelineLayout(poolData: Map<string, PoolData>) {
- return computeTimelineLayout(poolData, this.props.Document, this.childDocs, this.filterDocs,
+ return computeTimelineLayout(poolData, this.props.Document, this.childDocs, this.childDocs,
this.childLayoutPairs, [this.props.PanelWidth(), this.props.PanelHeight()], this.viewDefsToJSX);
}
doPivotLayout(poolData: Map<string, PoolData>) {
- return computePivotLayout(poolData, this.props.Document, this.childDocs, this.filterDocs,
+ return computePivotLayout(poolData, this.props.Document, this.childDocs, this.childDocs,
this.childLayoutPairs, [this.props.PanelWidth(), this.props.PanelHeight()], this.viewDefsToJSX);
}
@@ -934,42 +934,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
return { newPool, computedElementData: this.doFreeformLayout(newPool) };
}
- @computed get filterDocs() {
- const docFilters = Cast(this.props.Document._docFilters, listSpec("string"), []);
- const docRangeFilters = Cast(this.props.Document._docRangeFilters, listSpec("string"), []);
- const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields
- for (let i = 0; i < docFilters.length; i += 3) {
- const [key, value, modifiers] = docFilters.slice(i, i + 3);
- if (!filterFacets[key]) {
- filterFacets[key] = {};
- }
- filterFacets[key][value] = modifiers;
- }
- const filteredDocs = docFilters.length ? this.childDocs.filter(d => {
- for (const facetKey of Object.keys(filterFacets)) {
- const facet = filterFacets[facetKey];
- const satisfiesFacet = Object.keys(facet).some(value =>
- (facet[value] === "x") !== Doc.matchFieldValue(d, facetKey, value));
- if (!satisfiesFacet) {
- return false;
- }
- }
- return true;
- }) : this.childDocs;
- const rangeFilteredDocs = filteredDocs.filter(d => {
- for (let i = 0; i < docRangeFilters.length; i += 3) {
- const key = docRangeFilters[i];
- const min = Number(docRangeFilters[i + 1]);
- const max = Number(docRangeFilters[i + 2]);
- const val = Cast(d[key], "number", null);
- if (val !== undefined && (val < min || val > max)) {
- return false;
- }
- }
- return true;
- });
- return rangeFilteredDocs;
- }
childLayoutDocFunc = () => this.props.childLayoutTemplate?.() || Cast(this.props.Document.childLayoutTemplate, Doc, null);
get doLayoutComputation() {
const { newPool, computedElementData } = this.doInternalLayoutComputation;