aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/documents/Documents.ts5
-rw-r--r--src/client/views/CollectionMulticolumnView.scss7
-rw-r--r--src/client/views/CollectionMulticolumnView.tsx77
-rw-r--r--src/client/views/MainView.scss10
-rw-r--r--src/client/views/MainView.tsx2
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx33
-rw-r--r--src/client/views/collections/CollectionLinearView.scss (renamed from src/client/views/CollectionLinearView.scss)4
-rw-r--r--src/client/views/collections/CollectionLinearView.tsx (renamed from src/client/views/CollectionLinearView.tsx)22
-rw-r--r--src/client/views/collections/CollectionMulticolumnView.scss23
-rw-r--r--src/client/views/collections/CollectionMulticolumnView.tsx240
-rw-r--r--src/client/views/collections/CollectionPivotView.scss57
-rw-r--r--src/client/views/collections/CollectionPivotView.tsx118
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx2
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx23
-rw-r--r--src/client/views/collections/CollectionView.tsx44
-rw-r--r--src/server/ApiManagers/SessionManager.ts7
-rw-r--r--src/server/RouteManager.ts6
17 files changed, 531 insertions, 149 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index be678d765..eacdd8214 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -69,6 +69,8 @@ export interface DocumentOptions {
page?: number;
scale?: number;
fitWidth?: boolean;
+ fitToBox?: boolean; // whether a freeformview should zoom/scale to create a shrinkwrapped view of its contents
+ isDisplayPanel?: boolean; // whether the panel functions as GoldenLayout "stack" used to display documents
forceActive?: boolean;
preventTreeViewOpen?: boolean; // ignores the treeViewOpen Doc flag which allows a treeViewItem's expande/collapse state to be independent of other views of the same document in the tree view
layout?: string | Doc;
@@ -114,6 +116,9 @@ export interface DocumentOptions {
dropConverter?: ScriptField; // script to run when documents are dropped on this Document.
strokeWidth?: number;
color?: string;
+ treeViewHideTitle?: boolean; // whether to hide the title of a tree view
+ treeViewOpen?: boolean; // whether this document is expanded in a tree view
+ isFacetFilter?: boolean; // whether document functions as a facet filter in a tree view
limitHeight?: number; // maximum height for newly created (eg, from pasting) text documents
// [key: string]: Opt<Field>;
}
diff --git a/src/client/views/CollectionMulticolumnView.scss b/src/client/views/CollectionMulticolumnView.scss
deleted file mode 100644
index 84e80da4a..000000000
--- a/src/client/views/CollectionMulticolumnView.scss
+++ /dev/null
@@ -1,7 +0,0 @@
-.collectionMulticolumnView_outer,
-.collectionMulticolumnView_contents {
- width: 100%;
- height: 100%;
- overflow: hidden;
-}
-
diff --git a/src/client/views/CollectionMulticolumnView.tsx b/src/client/views/CollectionMulticolumnView.tsx
deleted file mode 100644
index 3231c0da1..000000000
--- a/src/client/views/CollectionMulticolumnView.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import { observer } from 'mobx-react';
-import { makeInterface, listSpec } from '../../new_fields/Schema';
-import { documentSchema } from '../../new_fields/documentSchemas';
-import { CollectionSubView } from './collections/CollectionSubView';
-import { DragManager } from '../util/DragManager';
-import * as React from "react";
-import { Doc, DocListCast } from '../../new_fields/Doc';
-import { NumCast, Cast, StrCast } from '../../new_fields/Types';
-import { List } from '../../new_fields/List';
-import { ContentFittingDocumentView } from './nodes/ContentFittingDocumentView';
-import { Utils } from '../../Utils';
-import { Transform } from '../util/Transform';
-import "./collectionMulticolumnView.scss";
-
-type MulticolumnDocument = makeInterface<[typeof documentSchema]>;
-const MulticolumnDocument = makeInterface(documentSchema);
-
-@observer
-export default class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocument) {
- private _dropDisposer?: DragManager.DragDropDisposer;
- private get configuration() {
- const { Document } = this.props;
- if (!Document.multicolumnData) {
- Document.multicolumnData = new List<Doc>();
- }
- return DocListCast(this.Document.multicolumnData);
- }
-
- protected createDropTarget = (ele: HTMLDivElement) => {
- this._dropDisposer && this._dropDisposer();
- if (ele) {
- this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this));
- }
- }
-
- getTransform = (ele: React.RefObject<HTMLDivElement>) => () => {
- if (!ele.current) return Transform.Identity();
- const { scale, translateX, translateY } = Utils.GetScreenTransform(ele.current);
- return new Transform(-translateX, -translateY, 1 / scale);
- }
-
- public isCurrent(doc: Doc) { return !doc.isMinimized && (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document.currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); }
-
- render() {
- const { PanelWidth } = this.props;
- return (
- <div className={"collectionMulticolumnView_outer"}>
- <div className={"collectionMulticolumnView_contents"}>
- {this.configuration.map(config => {
- const { target, columnWidth } = config;
- if (target instanceof Doc) {
- let computedWidth: number = 0;
- const widthSpecifier = Cast(columnWidth, "number");
- let matches: RegExpExecArray | null;
- if (widthSpecifier !== undefined) {
- computedWidth = widthSpecifier;
- } else if ((matches = /([\d.]+)\%/.exec(StrCast(columnWidth))) !== null) {
- computedWidth = Number(matches[1]) / 100 * PanelWidth();
- }
- return (!computedWidth ? (null) :
- <ContentFittingDocumentView
- {...this.props}
- Document={target}
- DataDocument={undefined}
- PanelWidth={() => computedWidth}
- getTransform={this.props.ScreenToLocalTransform}
- />
- );
- }
- return (null);
- })}
- </div>
- </div>
- );
- }
-
-} \ No newline at end of file
diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss
index 0bc07fa43..3c8073c54 100644
--- a/src/client/views/MainView.scss
+++ b/src/client/views/MainView.scss
@@ -5,10 +5,6 @@
.mainView-tabButtons {
position: relative;
width: 100%;
-
- .documentView-node-topmost {
- height: 200% !important;
- }
}
.mainContent-div {
@@ -17,12 +13,6 @@
height: 100%;
}
-.mainView-contentArea {
- .documentView-node-topmost {
- height: 200% !important;
- }
-}
-
// add nodes menu. Note that the + button is actually an input label, not an actual button.
.mainView-docButtons {
position: absolute;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index b300b0471..26c9b411d 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -22,7 +22,7 @@ import { Docs, DocumentOptions } from '../documents/Documents';
import { HistoryUtil } from '../util/History';
import SharingManager from '../util/SharingManager';
import { Transform } from '../util/Transform';
-import { CollectionLinearView } from './CollectionLinearView';
+import { CollectionLinearView } from './collections/CollectionLinearView';
import { CollectionViewType, CollectionView } from './collections/CollectionView';
import { CollectionDockingView } from './collections/CollectionDockingView';
import { ContextMenu } from './ContextMenu';
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 6c50ea0f2..022eccc13 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -207,6 +207,38 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
instance.layoutChanged();
return true;
}
+ //
+ // Creates a vertical split on the right side of the docking view, and then adds the Document to that split
+ //
+ @undoBatch
+ @action
+ public static UseRightSplit(document: Doc, dataDoc: Doc | undefined, libraryPath?: Doc[]) {
+ if (!CollectionDockingView.Instance) return false;
+ const instance = CollectionDockingView.Instance;
+ if (instance._goldenLayout.root.contentItems[0].isRow) {
+ let found: DocumentView | undefined;
+ Array.from(instance._goldenLayout.root.contentItems[0].contentItems).some((child: any) => {
+ if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" &&
+ DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)?.props.Document.isDisplayPanel) {
+ found = DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!;
+ } else {
+ Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => {
+ if (DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)?.props.Document.isDisplayPanel) {
+ found = DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!;
+ return true;
+ }
+ return false;
+ });
+ }
+ });
+ if (found) {
+ Doc.GetProto(found.props.Document).data = new List<Doc>([document]);
+ } else {
+ const stackView = Docs.Create.FreeformDocument([document], { fitToBox: true, isDisplayPanel: true, title: "document viewer" });
+ CollectionDockingView.AddRightSplit(stackView, undefined, []);
+ }
+ }
+ }
@undoBatch
@action
@@ -721,3 +753,4 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
}
}
Scripting.addGlobal(function openOnRight(doc: any) { CollectionDockingView.AddRightSplit(doc, undefined); });
+Scripting.addGlobal(function useRightSplit(doc: any) { CollectionDockingView.UseRightSplit(doc, undefined); });
diff --git a/src/client/views/CollectionLinearView.scss b/src/client/views/collections/CollectionLinearView.scss
index 81210d7ae..eae9e0220 100644
--- a/src/client/views/CollectionLinearView.scss
+++ b/src/client/views/collections/CollectionLinearView.scss
@@ -1,5 +1,5 @@
-@import "globalCssVariables";
-@import "nodeModuleOverrides";
+@import "../globalCssVariables";
+@import "../_nodeModuleOverrides";
.collectionLinearView-outer{
overflow: hidden;
diff --git a/src/client/views/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx
index 5ca861f71..9191bf822 100644
--- a/src/client/views/CollectionLinearView.tsx
+++ b/src/client/views/collections/CollectionLinearView.tsx
@@ -1,18 +1,18 @@
import { action, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { Doc, HeightSym, WidthSym } from '../../new_fields/Doc';
-import { makeInterface } from '../../new_fields/Schema';
-import { BoolCast, NumCast, StrCast } from '../../new_fields/Types';
-import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils } from '../../Utils';
-import { DragManager } from '../util/DragManager';
-import { Transform } from '../util/Transform';
+import { Doc, HeightSym, WidthSym } from '../../../new_fields/Doc';
+import { makeInterface } from '../../../new_fields/Schema';
+import { BoolCast, NumCast, StrCast } from '../../../new_fields/Types';
+import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils } from '../../../Utils';
+import { DragManager } from '../../util/DragManager';
+import { Transform } from '../../util/Transform';
import "./CollectionLinearView.scss";
-import { CollectionViewType } from './collections/CollectionView';
-import { CollectionSubView } from './collections/CollectionSubView';
-import { DocumentView } from './nodes/DocumentView';
-import { documentSchema } from '../../new_fields/documentSchemas';
-import { Id } from '../../new_fields/FieldSymbols';
+import { CollectionViewType } from './CollectionView';
+import { CollectionSubView } from './CollectionSubView';
+import { DocumentView } from '../nodes/DocumentView';
+import { documentSchema } from '../../../new_fields/documentSchemas';
+import { Id } from '../../../new_fields/FieldSymbols';
type LinearDocument = makeInterface<[typeof documentSchema,]>;
diff --git a/src/client/views/collections/CollectionMulticolumnView.scss b/src/client/views/collections/CollectionMulticolumnView.scss
new file mode 100644
index 000000000..120603a0b
--- /dev/null
+++ b/src/client/views/collections/CollectionMulticolumnView.scss
@@ -0,0 +1,23 @@
+.collectionMulticolumnView_contents {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+
+ .fish {
+ display: flex;
+ flex-direction: column;
+
+ .display {
+ text-align: center;
+ height: 20px;
+ }
+
+ }
+
+ .resizer {
+ background: black;
+ cursor: ew-resize;
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionMulticolumnView.tsx b/src/client/views/collections/CollectionMulticolumnView.tsx
new file mode 100644
index 000000000..51064d5c3
--- /dev/null
+++ b/src/client/views/collections/CollectionMulticolumnView.tsx
@@ -0,0 +1,240 @@
+import { observer } from 'mobx-react';
+import { makeInterface } from '../../../new_fields/Schema';
+import { documentSchema } from '../../../new_fields/documentSchemas';
+import { CollectionSubView } from './CollectionSubView';
+import * as React from "react";
+import { Doc } from '../../../new_fields/Doc';
+import { NumCast, StrCast } from '../../../new_fields/Types';
+import { ContentFittingDocumentView } from './../nodes/ContentFittingDocumentView';
+import { Utils } from '../../../Utils';
+import "./collectionMulticolumnView.scss";
+import { computed } from 'mobx';
+
+type MulticolumnDocument = makeInterface<[typeof documentSchema]>;
+const MulticolumnDocument = makeInterface(documentSchema);
+
+interface Unresolved {
+ target: Doc;
+ magnitude: number;
+ unit: string;
+}
+
+interface Resolved {
+ target: Doc;
+ pixels: number;
+}
+
+interface LayoutData {
+ unresolved: Unresolved[];
+ numFixed: number;
+ numRatio: number;
+ starSum: number;
+}
+
+const resolvedUnits = ["*", "px"];
+const resizerWidth = 2;
+
+@observer
+export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocument) {
+
+ @computed
+ private get resolvedLayoutInformation(): LayoutData {
+ const unresolved: Unresolved[] = [];
+ let starSum = 0, numFixed = 0, numRatio = 0;
+ for (const pair of this.childLayoutPairs) {
+ const unit = StrCast(pair.layout.widthUnit);
+ const magnitude = NumCast(pair.layout.widthMagnitude);
+ if (unit && magnitude && magnitude > 0 && resolvedUnits.includes(unit)) {
+ if (unit === "*") {
+ starSum += magnitude;
+ numRatio++;
+ } else {
+ numFixed++;
+ }
+ unresolved.push({ target: pair.layout, magnitude, unit });
+ }
+ // otherwise, the particular configuration entry is ignored and the remaining
+ // space is allocated as if the document were absent from the configuration list
+ }
+ return { unresolved, numRatio, numFixed, starSum };
+ }
+
+ /**
+ * This returns the total quantity, in pixels, that this
+ * view needs to reserve for child documents that have
+ * (with higher priority) requested a fixed pixel width.
+ *
+ * If the underlying resolvedLayoutInformation returns null
+ * because we're waiting on promises to resolve, this value will be undefined as well.
+ */
+ @computed
+ private get totalFixedAllocation(): number | undefined {
+ return this.resolvedLayoutInformation?.unresolved.reduce(
+ (sum, { magnitude, unit }) => sum + (unit === "px" ? magnitude : 0), 0);
+ }
+
+ /**
+ * This returns the total quantity, in pixels, that this
+ * view needs to reserve for child documents that have
+ * (with lower priority) requested a certain relative proportion of the
+ * remaining pixel width not allocated for fixed widths.
+ *
+ * If the underlying totalFixedAllocation returns undefined
+ * because we're waiting indirectly on promises to resolve, this value will be undefined as well.
+ */
+ @computed
+ private get totalRatioAllocation(): number | undefined {
+ const layoutInfoLen = this.resolvedLayoutInformation?.unresolved.length;
+ if (layoutInfoLen > 0 && this.totalFixedAllocation !== undefined) {
+ return this.props.PanelWidth() - (this.totalFixedAllocation + resizerWidth * (layoutInfoLen - 1));
+ }
+ }
+
+ /**
+ * This returns the total quantity, in pixels, that
+ * 1* (relative / star unit) is worth. For example,
+ * if the configuration has three documents, with, respectively,
+ * widths of 2*, 2* and 1*, and the panel width returns 1000px,
+ * this accessor returns 1000 / (2 + 2 + 1), or 200px.
+ * Elsewhere, this is then multiplied by each relative-width
+ * document's (potentially decimal) * count to compute its actual width (400px, 400px and 200px).
+ *
+ * If the underlying totalRatioAllocation or this.resolveLayoutInformation return undefined
+ * because we're waiting indirectly on promises to resolve, this value will be undefined as well.
+ */
+ @computed
+ private get columnUnitLength(): number | undefined {
+ if (this.resolvedLayoutInformation && this.totalRatioAllocation !== undefined) {
+ return this.totalRatioAllocation / this.resolvedLayoutInformation.starSum;
+ }
+ }
+
+ @computed
+ private get contents(): JSX.Element[] | null {
+ const layout = this.resolvedLayoutInformation;
+ const columnUnitLength = this.columnUnitLength;
+ if (layout === null || columnUnitLength === undefined) {
+ return (null); // we're still waiting on promises to resolve
+ }
+ const resolved: Resolved[] = [];
+ layout.unresolved.forEach(item => {
+ const { unit, magnitude, ...remaining } = item;
+ let width = magnitude;
+ if (unit === "*") {
+ width = magnitude * columnUnitLength;
+ }
+ resolved.push({ pixels: width, ...remaining });
+ });
+ const collector: JSX.Element[] = [];
+ let offset = 0;
+ for (let i = 0; i < resolved.length; i++) {
+ const { target, pixels } = resolved[i];
+ const shiftX = offset;
+ collector.push(
+ <div className={"fish"}>
+ <ContentFittingDocumentView
+ {...this.props}
+ key={Utils.GenerateGuid()}
+ Document={target}
+ DataDocument={target.resolvedDataDoc as Doc}
+ PanelWidth={() => pixels}
+ getTransform={() => this.props.ScreenToLocalTransform().translate(-shiftX, 0)}
+ />
+ <span className={"display"}>{NumCast(target.widthMagnitude).toFixed(3)} {StrCast(target.widthUnit)}</span>
+ </div>,
+ <ResizeBar
+ width={resizerWidth}
+ key={Utils.GenerateGuid()}
+ columnUnitLength={columnUnitLength}
+ toLeft={target}
+ toRight={resolved[i + 1]?.target}
+ />
+ );
+ offset += pixels + resizerWidth;
+ }
+ collector.pop(); // removes the final extraneous resize bar
+ return collector;
+ }
+
+ render(): JSX.Element {
+ return (
+ <div
+ className={"collectionMulticolumnView_contents"}
+ ref={this.createDropTarget}
+ >
+ {this.contents}
+ </div>
+ );
+ }
+
+}
+
+interface SpacerProps {
+ width: number;
+ columnUnitLength: number;
+ toLeft?: Doc;
+ toRight?: Doc;
+}
+
+class ResizeBar extends React.Component<SpacerProps> {
+
+ private registerResizing = (e: React.PointerEvent<HTMLDivElement>) => {
+ e.stopPropagation();
+ e.preventDefault();
+ window.removeEventListener("pointermove", this.onPointerMove);
+ window.removeEventListener("pointerup", this.onPointerUp);
+ window.addEventListener("pointermove", this.onPointerMove);
+ window.addEventListener("pointerup", this.onPointerUp);
+ }
+
+ private onPointerMove = ({ movementX }: PointerEvent) => {
+ const { toLeft, toRight, columnUnitLength } = this.props;
+ const target = movementX > 0 ? toRight : toLeft;
+ if (target) {
+ const { widthUnit, widthMagnitude } = target;
+ if (widthUnit === "*") {
+ target.widthMagnitude = NumCast(widthMagnitude) - Math.abs(movementX) / columnUnitLength;
+ }
+ }
+ }
+
+ private get opacity() {
+ const { toLeft, toRight } = this.props;
+ if (toLeft && toRight) {
+ if (StrCast(toLeft.widthUnit) === "px" && StrCast(toRight.widthUnit) === "px") {
+ return 0;
+ }
+ return 0.4;
+ } else if (toLeft) {
+ if (StrCast(toLeft.widthUnit) === "px") {
+ return 0;
+ }
+ return 0.4;
+ } else if (toRight) {
+ if (StrCast(toRight.widthUnit) === "px") {
+ return 0;
+ }
+ return 0.4;
+ }
+ return 0;
+ }
+
+ private onPointerUp = () => {
+ window.removeEventListener("pointermove", this.onPointerMove);
+ window.removeEventListener("pointerup", this.onPointerUp);
+ }
+
+ render() {
+ return (
+ <div
+ className={"resizer"}
+ style={{
+ width: this.props.width,
+ opacity: this.opacity
+ }}
+ onPointerDown={this.registerResizing}
+ />
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionPivotView.scss b/src/client/views/collections/CollectionPivotView.scss
new file mode 100644
index 000000000..bd3d6c77b
--- /dev/null
+++ b/src/client/views/collections/CollectionPivotView.scss
@@ -0,0 +1,57 @@
+.collectionPivotView {
+ display: flex;
+ flex-direction: row;
+ position: absolute;
+ height:100%;
+ width:100%;
+ .collectionPivotView-flyout {
+ width: 400px;
+ height: 300px;
+ display: inline-block;
+ .collectionPivotView-flyout-item {
+ background-color: lightgray;
+ text-align: left;
+ display: inline-block;
+ position: relative;
+ width: 100%;
+ }
+ }
+
+ .collectionPivotView-treeView {
+ display:flex;
+ flex-direction: column;
+ width: 200px;
+ height: 100%;
+ .collectionPivotView-addfacet {
+ display:inline-block;
+ width: 200px;
+ height: 30px;
+ background: darkGray;
+ text-align: center;
+ .collectionPivotView-button {
+ align-items: center;
+ display: flex;
+ width: 100%;
+ height: 100%;
+ .collectionPivotView-span {
+ margin: auto;
+ }
+ }
+ > div, > div > div {
+ width: 100%;
+ height: 100%;
+ text-align: center;
+ }
+ }
+ .collectionPivotView-tree {
+ display:inline-block;
+ width: 200px;
+ height: calc(100% - 30px);
+ }
+ }
+ .collectionPivotView-pivot {
+ display:inline-block;
+ width: calc(100% - 200px);
+ height: 100%;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionPivotView.tsx b/src/client/views/collections/CollectionPivotView.tsx
new file mode 100644
index 000000000..6af7cce70
--- /dev/null
+++ b/src/client/views/collections/CollectionPivotView.tsx
@@ -0,0 +1,118 @@
+import { CollectionSubView } from "./CollectionSubView";
+import React = require("react");
+import { computed, action, IReactionDisposer, reaction, runInAction, observable } from "mobx";
+import { faEdit, faChevronCircleUp } from "@fortawesome/free-solid-svg-icons";
+import { Doc, DocListCast } from "../../../new_fields/Doc";
+import "./CollectionPivotView.scss";
+import { observer } from "mobx-react";
+import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
+import { CollectionTreeView } from "./CollectionTreeView";
+import { Cast, StrCast, NumCast } from "../../../new_fields/Types";
+import { Docs } from "../../documents/Documents";
+import { ScriptField } from "../../../new_fields/ScriptField";
+import { CompileScript } from "../../util/Scripting";
+import { anchorPoints, Flyout } from "../TemplateMenu";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { List } from "../../../new_fields/List";
+import { Set } from "typescript-collections";
+
+@observer
+export class CollectionPivotView extends CollectionSubView(doc => doc) {
+ componentDidMount = () => {
+ this.props.Document.freeformLayoutEngine = "pivot";
+ if (!this.props.Document.facetCollection) {
+ const facetCollection = Docs.Create.FreeformDocument([], { title: "facetFilters", yMargin: 0, treeViewHideTitle: true });
+ facetCollection.target = this.props.Document;
+
+ const scriptText = "setDocFilter(context.target, heading, this.title, checked)";
+ const script = CompileScript(scriptText, {
+ params: { this: Doc.name, heading: "boolean", checked: "boolean", context: Doc.name },
+ typecheck: false,
+ editable: true,
+ });
+ if (script.compiled) {
+ facetCollection.onCheckedClick = new ScriptField(script);
+ }
+
+ const openDocText = "const alias = getAlias(this); alias.layoutKey = 'layoutCustom'; useRightSplit(alias); ";
+ const openDocScript = CompileScript(openDocText, {
+ params: { this: Doc.name, heading: "boolean", checked: "boolean", context: Doc.name },
+ typecheck: false,
+ editable: true,
+ });
+ if (openDocScript.compiled) {
+ this.props.Document.onChildClick = new ScriptField(openDocScript);
+ }
+
+ this.props.Document.facetCollection = facetCollection;
+ this.props.Document.fitToBox = true;
+ }
+ }
+
+ @computed get fieldExtensionDoc() {
+ return Doc.fieldExtensionDoc(this.props.DataDoc || this.props.Document, this.props.fieldKey);
+ }
+
+ bodyPanelWidth = () => this.props.PanelWidth() - 200;
+ getTransform = () => this.props.ScreenToLocalTransform().translate(-200, 0);
+
+ @computed get _allFacets() {
+ const facets = new Set<string>();
+ this.childDocs.forEach(child => Object.keys(Doc.GetProto(child)).forEach(key => facets.add(key)));
+ return facets.toArray();
+ }
+
+ facetClick = (facet: string) => {
+ const facetCollection = this.props.Document.facetCollection;
+ if (facetCollection instanceof Doc) {
+ const found = DocListCast(facetCollection.data).findIndex(doc => doc.title === facet);
+ if (found !== -1) {
+ //Doc.RemoveDocFromList(facetCollection, "data", DocListCast(facetCollection.data)[found]);
+ (facetCollection.data as List<Doc>).splice(found, 1);
+ } else {
+ const facetValues = new Set<string>();
+ this.childDocs.forEach(child => {
+ Object.keys(Doc.GetProto(child)).forEach(key => child[key] instanceof Doc && facetValues.add((child[key] as Doc)[facet]?.toString() || "(null)"));
+ facetValues.add(child[facet]?.toString() || "(null)");
+ });
+
+ const newFacetVals = facetValues.toArray().map(val => Docs.Create.TextDocument({ title: val.toString() }));
+ const newFacet = Docs.Create.FreeformDocument(newFacetVals, { title: facet, treeViewOpen: true, isFacetFilter: true });
+ Doc.AddDocToList(facetCollection, "data", newFacet);
+ }
+ }
+ }
+
+ render() {
+ const facetCollection = Cast(this.props.Document?.facetCollection, Doc, null);
+ const flyout = (
+ <div className="collectionPivotView-flyout" title=" ">
+ {this._allFacets.map(facet => <label className="collectionPivotView-flyout-item" onClick={e => this.facetClick(facet)}>
+ <input type="checkbox" checked={this.props.Document.facetCollection instanceof Doc && DocListCast(this.props.Document.facetCollection.data).some(d => {
+ return d.title === facet;
+ })} />
+ <span className="checkmark" />
+ {facet}
+ </label>)}
+ </div>
+ );
+ return !facetCollection ? (null) : <div className="collectionPivotView">
+ <div className="collectionPivotView-treeView">
+ <div className="collectionPivotView-addFacet" onPointerDown={e => e.stopPropagation()}>
+ <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}>
+ <div className="collectionPivotView-button">
+ <span className="collectionPivotView-span">Facet Filters</span>
+ <FontAwesomeIcon icon={faEdit} size={"lg"} />
+ </div>
+ </Flyout>
+ </div>
+ <div className="collectionPivotView-tree">
+ <CollectionTreeView {...this.props} Document={facetCollection} />
+ </div>
+ </div>
+ <div className="collectionPivotView-pivot">
+ <CollectionFreeFormView {...this.props} ScreenToLocalTransform={this.getTransform} PanelWidth={this.bodyPanelWidth} />
+ </div>
+ </div>;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 886a9c870..ca792c134 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -396,7 +396,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
ref={this.createRef}
style={{
transform: `scale(${Math.min(1, this.props.PanelHeight() / this.layoutDoc[HeightSym]())})`,
- height: `${100 * 1 / Math.min(this.props.PanelWidth() / this.layoutDoc[WidthSym](), this.props.PanelHeight() / this.layoutDoc[HeightSym]())}%`,
+ height: `${Math.max(100, 100 * 1 / Math.min(this.props.PanelWidth() / this.layoutDoc[WidthSym](), this.props.PanelHeight() / this.layoutDoc[HeightSym]()))}%`,
transformOrigin: "top"
}}
onScroll={action((e: React.UIEvent<HTMLDivElement>) => this._scroll = e.currentTarget.scrollTop)}
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 3356aed68..70860b6bd 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -52,7 +52,7 @@ export interface TreeViewProps {
outdentDocument?: () => void;
ScreenToLocalTransform: () => Transform;
outerXf: () => { translateX: number, translateY: number };
- treeViewId: string;
+ treeViewId: Doc;
parentKey: string;
active: (outsideReaction?: boolean) => boolean;
hideHeaderFields: () => boolean;
@@ -234,8 +234,8 @@ class TreeView extends React.Component<TreeViewProps> {
if (inside) {
addDoc = (doc: Doc) => Doc.AddDocToList(this.dataDoc, this.fieldKey, doc) || addDoc(doc);
}
- const movedDocs = (de.complete.docDragData.treeViewId === this.props.treeViewId ? de.complete.docDragData.draggedDocuments : de.complete.docDragData.droppedDocuments);
- return ((de.complete.docDragData.dropAction && (de.complete.docDragData.treeViewId !== this.props.treeViewId)) || de.complete.docDragData.userDropAction) ?
+ const movedDocs = (de.complete.docDragData.treeViewId === this.props.treeViewId[Id] ? de.complete.docDragData.draggedDocuments : de.complete.docDragData.droppedDocuments);
+ return ((de.complete.docDragData.dropAction && (de.complete.docDragData.treeViewId !== this.props.treeViewId[Id])) || de.complete.docDragData.userDropAction) ?
de.complete.docDragData.droppedDocuments.reduce((added, d) => addDoc(d) || added, false)
: de.complete.docDragData.moveDocument ?
movedDocs.reduce((added, d) => de.complete.docDragData?.moveDocument?.(d, undefined, addDoc) || added, false)
@@ -368,12 +368,13 @@ class TreeView extends React.Component<TreeViewProps> {
@action
bulletClick = (e: React.MouseEvent) => {
- if (this.props.onCheckedClick) {
+ if (this.props.onCheckedClick && this.props.document.type !== DocumentType.COL) {
this.props.document.treeViewChecked = this.props.document.treeViewChecked === "check" ? "x" : this.props.document.treeViewChecked === "x" ? undefined : "check";
ScriptCast(this.props.onCheckedClick).script.run({
this: this.props.document.isTemplateField && this.props.dataDoc ? this.props.dataDoc : this.props.document,
heading: this.props.containingCollection.title,
- checked: this.props.document.treeViewChecked === "check" ? false : this.props.document.treeViewChecked === "x" ? "x" : "none"
+ checked: this.props.document.treeViewChecked === "check" ? false : this.props.document.treeViewChecked === "x" ? "x" : "none",
+ context: this.props.treeViewId
}, console.log);
} else {
this.treeViewOpen = !this.treeViewOpen;
@@ -383,7 +384,7 @@ class TreeView extends React.Component<TreeViewProps> {
@computed
get renderBullet() {
- const checked = this.props.onCheckedClick ? (this.props.document.treeViewChecked ? this.props.document.treeViewChecked : "unchecked") : undefined;
+ const checked = this.props.document.type === DocumentType.COL ? undefined : this.props.onCheckedClick ? (this.props.document.treeViewChecked ? this.props.document.treeViewChecked : "unchecked") : undefined;
return <div className="bullet" title="view inline" onClick={this.bulletClick} style={{ color: StrCast(this.props.document.color, checked === "unchecked" ? "white" : "black"), opacity: 0.4 }}>
{<FontAwesomeIcon icon={checked === "check" ? "check" : (checked === "x" ? "times" : checked === "unchecked" ? "square" : !this.treeViewOpen ? (this.childDocs ? "caret-square-right" : "caret-right") : (this.childDocs ? "caret-square-down" : "caret-down"))} />}
</div>;
@@ -394,7 +395,7 @@ class TreeView extends React.Component<TreeViewProps> {
@computed
get renderTitle() {
const reference = React.createRef<HTMLDivElement>();
- const onItemDown = SetupDrag(reference, () => this.dataDoc, this.move, this.props.dropAction, this.props.treeViewId, true);
+ const onItemDown = SetupDrag(reference, () => this.dataDoc, this.move, this.props.dropAction, this.props.treeViewId[Id], true);
const headerElements = (
<span className="collectionTreeView-keyHeader" key={this.treeViewExpandedView}
@@ -444,7 +445,7 @@ class TreeView extends React.Component<TreeViewProps> {
}
public static GetChildElements(
childDocs: Doc[],
- treeViewId: string,
+ treeViewId: Doc,
containingCollection: Doc,
dataDoc: Doc | undefined,
key: string,
@@ -655,7 +656,7 @@ export class CollectionTreeView extends CollectionSubView(Document) {
onWheel={(e: React.WheelEvent) => this._mainEle && this._mainEle.scrollHeight > this._mainEle.clientHeight && e.stopPropagation()}
onDrop={this.onTreeDrop}
ref={this.createTreeDropTarget}>
- <EditableView
+ {(this.props.Document.treeViewHideTitle ? (null) : <EditableView
contents={this.dataDoc.title}
display={"block"}
maxHeight={72}
@@ -668,11 +669,11 @@ export class CollectionTreeView extends CollectionSubView(Document) {
const doc = layoutDoc || Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
TreeView.loadId = doc[Id];
Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, this.childDocs.length ? this.childDocs[0] : undefined, true, false, false, false);
- })} />
+ })} />)}
{this.props.Document.allowClear ? this.renderClearButton : (null)}
<ul className="no-indent" style={{ width: "max-content" }} >
{
- TreeView.GetChildElements(this.childDocs, this.props.Document[Id], this.props.Document, this.props.DataDoc, this.props.fieldKey, this.props.ContainingCollectionDoc, undefined, addDoc, this.remove,
+ TreeView.GetChildElements(this.childDocs, this.props.Document, this.props.Document, this.props.DataDoc, this.props.fieldKey, this.props.ContainingCollectionDoc, undefined, addDoc, this.remove,
moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform,
this.outerXf, this.props.active, this.props.PanelWidth, this.props.ChromeHeight, this.props.renderDepth, () => BoolCast(this.props.Document.hideHeaderFields),
BoolCast(this.props.Document.preventTreeViewOpen), [], this.props.LibraryPath, ScriptCast(this.props.Document.onCheckedClick))
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 21371dd39..4bd456233 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -4,38 +4,38 @@ import { faColumns, faCopy, faEllipsisV, faFingerprint, faImage, faProjectDiagra
import { action, IReactionDisposer, observable, reaction, runInAction } 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 { Doc, DocListCast } from '../../../new_fields/Doc';
import { Id } from '../../../new_fields/FieldSymbols';
-import { StrCast, BoolCast, Cast } from '../../../new_fields/Types';
+import { listSpec } from '../../../new_fields/Schema';
+import { BoolCast, Cast, StrCast } from '../../../new_fields/Types';
+import { ImageField } from '../../../new_fields/URLField';
+import { TraceMobx } from '../../../new_fields/util';
import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
+import { Utils } from '../../../Utils';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { DocumentManager } from '../../util/DocumentManager';
+import { ImageUtils } from '../../util/Import & Export/ImageUtils';
+import { SelectionManager } from '../../util/SelectionManager';
import { ContextMenu } from "../ContextMenu";
+import { FieldView, FieldViewProps } from '../nodes/FieldView';
+import { ScriptBox } from '../ScriptBox';
+import { Touchable } from '../Touchable';
import { CollectionDockingView } from "./CollectionDockingView";
import { AddCustomFreeFormLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines';
import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
+import { CollectionLinearView } from './CollectionLinearView';
+import { CollectionMulticolumnView } from './CollectionMulticolumnView';
+import { CollectionPivotView } from './CollectionPivotView';
import { CollectionSchemaView } from "./CollectionSchemaView";
import { CollectionStackingView } from './CollectionStackingView';
+import { CollectionStaffView } from './CollectionStaffView';
import { CollectionTreeView } from "./CollectionTreeView";
+import './CollectionView.scss';
import { CollectionViewBaseChrome } from './CollectionViewChromes';
-import { ImageUtils } from '../../util/Import & Export/ImageUtils';
-import { CollectionLinearView } from '../CollectionLinearView';
-import { CollectionStaffView } from './CollectionStaffView';
-import { DocumentType } from '../../documents/DocumentTypes';
-import { ImageField } from '../../../new_fields/URLField';
-import { DocListCast } from '../../../new_fields/Doc';
-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
export const COLLECTION_BORDER_WIDTH = 2;
-import { DateField } from '../../../new_fields/DateField';
-import { Doc, } from '../../../new_fields/Doc';
-import { listSpec } from '../../../new_fields/Schema';
-import { DocumentManager } from '../../util/DocumentManager';
-import { SelectionManager } from '../../util/SelectionManager';
-import './CollectionView.scss';
-import { FieldViewProps, FieldView } from '../nodes/FieldView';
-import { Touchable } from '../Touchable';
-import { TraceMobx } from '../../../new_fields/util';
-import { Utils } from '../../../Utils';
-import { ScriptBox } from '../ScriptBox';
-import CollectionMulticolumnView from '../CollectionMulticolumnView';
const path = require('path');
library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any, faCopy);
@@ -180,7 +180,7 @@ export class CollectionView extends Touchable<FieldViewProps> {
case CollectionViewType.Linear: { return (<CollectionLinearView key="collview" {...props} />); }
case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView key="collview" {...props} />); }
case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView key="collview" {...props} />); }
- case CollectionViewType.Pivot: { this.props.Document.freeformLayoutEngine = "pivot"; return (<CollectionFreeFormView key="collview" {...props} />); }
+ case CollectionViewType.Pivot: { return (<CollectionPivotView key="collview" {...props} />); }
case CollectionViewType.Freeform:
default: { this.props.Document.freeformLayoutEngine = undefined; return (<CollectionFreeFormView key="collview" {...props} />); }
}
diff --git a/src/server/ApiManagers/SessionManager.ts b/src/server/ApiManagers/SessionManager.ts
index a99aa05e0..f1629b8f0 100644
--- a/src/server/ApiManagers/SessionManager.ts
+++ b/src/server/ApiManagers/SessionManager.ts
@@ -8,16 +8,15 @@ const permissionError = "You are not authorized!";
export default class SessionManager extends ApiManager {
- private secureSubscriber = (root: string, ...params: string[]) => new RouteSubscriber(root).add("sessionKey", ...params);
+ private secureSubscriber = (root: string, ...params: string[]) => new RouteSubscriber(root).add("session_key", ...params);
private authorizedAction = (handler: SecureHandler) => {
return (core: AuthorizedCore) => {
- const { req, res, isRelease } = core;
- const { sessionKey } = req.params;
+ const { req: { params }, res, isRelease } = core;
if (!isRelease) {
return res.send("This can be run only on the release server.");
}
- if (sessionKey !== process.env.session_key) {
+ if (params.session_key !== process.env.session_key) {
return _permission_denied(res, permissionError);
}
return handler(core);
diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts
index d072b7709..6bc75ca21 100644
--- a/src/server/RouteManager.ts
+++ b/src/server/RouteManager.ts
@@ -68,7 +68,7 @@ export default class RouteManager {
console.log('please remove all duplicate routes before continuing');
}
if (malformedCount) {
- console.log(`please ensure all routes adhere to ^\/$|^\/[A-Za-z]+(\/\:[A-Za-z?]+)*$`);
+ console.log(`please ensure all routes adhere to ^\/$|^\/[A-Za-z]+(\/\:[A-Za-z?_]+)*$`);
}
process.exit(1);
} else {
@@ -133,7 +133,7 @@ export default class RouteManager {
} else {
route = subscriber.build;
}
- if (!/^\/$|^\/[A-Za-z]+(\/\:[A-Za-z?]+)*$/g.test(route)) {
+ if (!/^\/$|^\/[A-Za-z]+(\/\:[A-Za-z?_]+)*$/g.test(route)) {
this.failedRegistrations.push({
reason: RegistrationError.Malformed,
route
@@ -198,5 +198,5 @@ export function _permission_denied(res: Response, message?: string) {
if (message) {
res.statusMessage = message;
}
- res.status(STATUS.BAD_REQUEST).send("Permission Denied!");
+ res.status(STATUS.PERMISSION_DENIED).send("Permission Denied!");
}