aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/documents/Documents.ts15
-rw-r--r--src/client/util/DragManager.ts16
-rw-r--r--src/client/views/EditableView.scss10
-rw-r--r--src/client/views/EditableView.tsx45
-rw-r--r--src/client/views/MainView.tsx7
-rw-r--r--src/client/views/MetadataEntryMenu.tsx1
-rw-r--r--src/client/views/collections/CollectionBaseView.tsx2
-rw-r--r--src/client/views/collections/CollectionSchemaCells.tsx289
-rw-r--r--src/client/views/collections/CollectionSchemaHeaders.tsx299
-rw-r--r--src/client/views/collections/CollectionSchemaMovableTableHOC.tsx231
-rw-r--r--src/client/views/collections/CollectionSchemaView.scss610
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx956
-rw-r--r--src/client/views/collections/CollectionStackingView.scss143
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx176
-rw-r--r--src/client/views/collections/CollectionStackingViewFieldColumn.tsx301
-rw-r--r--src/client/views/collections/CollectionSubView.tsx16
-rw-r--r--src/client/views/collections/CollectionTreeView.scss27
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx2
-rw-r--r--src/client/views/collections/CollectionView.tsx48
-rw-r--r--src/client/views/collections/CollectionViewChromes.scss168
-rw-r--r--src/client/views/collections/CollectionViewChromes.tsx373
-rw-r--r--src/client/views/collections/KeyRestrictionRow.tsx45
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx3
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx3
-rw-r--r--src/client/views/nodes/DocumentView.tsx7
-rw-r--r--src/client/views/nodes/LinkEditor.tsx3
-rw-r--r--src/client/views/nodes/LinkMenuGroup.tsx3
-rw-r--r--src/new_fields/Doc.ts9
-rw-r--r--src/new_fields/SchemaHeaderField.ts87
-rw-r--r--src/server/authentication/models/current_user_utils.ts4
30 files changed, 3200 insertions, 699 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 3859f2255..e6fe1b8b3 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -39,6 +39,7 @@ import { DocumentManager } from "../util/DocumentManager";
import DirectoryImportBox from "../util/Import & Export/DirectoryImportBox";
import { Scripting } from "../util/Scripting";
import { ButtonBox } from "../views/nodes/ButtonBox";
+import { SchemaHeaderField, RandomPastel } from "../../new_fields/SchemaHeaderField";
var requestImageSize = require('../util/request-image-size');
var path = require('path');
@@ -84,7 +85,7 @@ export interface DocumentOptions {
curPage?: number;
documentText?: string;
borderRounding?: string;
- schemaColumns?: List<string>;
+ schemaColumns?: List<SchemaHeaderField>;
dockingConfig?: string;
dbDoc?: Doc;
// [key: string]: Opt<Field>;
@@ -403,19 +404,23 @@ export namespace Docs {
}
export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Freeform });
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaColumns: new List([new SchemaHeaderField("title")]), ...options, viewType: CollectionViewType.Freeform });
}
- export function SchemaDocument(schemaColumns: string[], documents: Array<Doc>, options: DocumentOptions) {
+ export function SchemaDocument(schemaColumns: SchemaHeaderField[], documents: Array<Doc>, options: DocumentOptions) {
return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaColumns: new List(schemaColumns), ...options, viewType: CollectionViewType.Schema });
}
export function TreeDocument(documents: Array<Doc>, options: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Tree });
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaColumns: new List([new SchemaHeaderField("title")]), ...options, viewType: CollectionViewType.Tree });
}
export function StackingDocument(documents: Array<Doc>, options: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Stacking });
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaColumns: new List([new SchemaHeaderField("title")]), ...options, viewType: CollectionViewType.Stacking });
+ }
+
+ export function MasonryDocument(documents: Array<Doc>, options: DocumentOptions) {
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaColumns: new List([new SchemaHeaderField("title")]), ...options, viewType: CollectionViewType.Masonry });
}
export function ButtonDocument(options?: DocumentOptions) {
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 95416cd53..5a4fa3688 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,6 +1,6 @@
import { action, runInAction } from "mobx";
import { Doc } from "../../new_fields/Doc";
-import { Cast } from "../../new_fields/Types";
+import { Cast, StrCast } from "../../new_fields/Types";
import { URLField } from "../../new_fields/URLField";
import { emptyFunction } from "../../Utils";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
@@ -8,6 +8,7 @@ import * as globalCssVariables from "../views/globalCssVariables.scss";
import { DocumentManager } from "./DocumentManager";
import { LinkManager } from "./LinkManager";
import { SelectionManager } from "./SelectionManager";
+import { SchemaHeaderField } from "../../new_fields/SchemaHeaderField";
import { DocumentDecorations } from "../views/DocumentDecorations";
export type dropActionType = "alias" | "copy" | undefined;
@@ -289,6 +290,15 @@ export namespace DragManager {
[id: string]: any;
}
+ // for column dragging in schema view
+ export class ColumnDragData {
+ constructor(colKey: SchemaHeaderField) {
+ this.colKey = colKey;
+ }
+ colKey: SchemaHeaderField;
+ [id: string]: any;
+ }
+
export function StartLinkDrag(ele: HTMLElement, dragData: LinkDragData, downX: number, downY: number, options?: DragOptions) {
StartDrag([ele], dragData, downX, downY, options);
}
@@ -297,6 +307,10 @@ export namespace DragManager {
StartDrag([ele], dragData, downX, downY, options);
}
+ export function StartColumnDrag(ele: HTMLElement, dragData: ColumnDragData, downX: number, downY: number, options?: DragOptions) {
+ StartDrag([ele], dragData, downX, downY, options);
+ }
+
export let AbortDrag: () => void = emptyFunction;
function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: { [id: string]: any }) => void) {
diff --git a/src/client/views/EditableView.scss b/src/client/views/EditableView.scss
index a5150cd66..19512362e 100644
--- a/src/client/views/EditableView.scss
+++ b/src/client/views/EditableView.scss
@@ -1,20 +1,24 @@
-.editableView-container-editing, .editableView-container-editing-oneLine {
+.editableView-container-editing,
+.editableView-container-editing-oneLine {
overflow-wrap: break-word;
word-wrap: break-word;
hyphens: auto;
overflow: hidden;
}
+
.editableView-container-editing-oneLine {
span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
- display:block;
+ display: block;
}
+
input {
- display:block;
+ display: block;
}
}
+
.editableView-input {
width: 100%;
background: inherit;
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index 7cabebddd..373b63282 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -2,6 +2,7 @@ import React = require('react');
import { observer } from 'mobx-react';
import { observable, action, trace } from 'mobx';
import "./EditableView.scss";
+import * as Autosuggest from 'react-autosuggest';
export interface EditableProps {
/**
@@ -28,9 +29,17 @@ export interface EditableProps {
fontSize?: number;
height?: number;
display?: string;
+ autosuggestProps?: {
+ resetValue: () => void;
+ value: string,
+ onChange: (e: React.ChangeEvent, { newValue }: { newValue: string }) => void,
+ autosuggestProps: Autosuggest.AutosuggestProps<string>
+
+ };
oneLine?: boolean;
editing?: boolean;
onClick?: (e: React.MouseEvent) => boolean;
+ isEditingCallback?: (isEditing: boolean) => void;
}
/**
@@ -48,6 +57,16 @@ export class EditableView extends React.Component<EditableProps> {
}
@action
+ componentWillReceiveProps(nextProps: EditableProps) {
+ // this is done because when autosuggest is turned on, the suggestions are passed in as a prop,
+ // so when the suggestions are passed in, and no editing prop is passed in, it used to set it
+ // to false. this will no longer do so -syip
+ if (nextProps.editing && nextProps.editing !== this._editing) {
+ this._editing = nextProps.editing;
+ }
+ }
+
+ @action
onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Tab") {
e.stopPropagation();
@@ -57,14 +76,17 @@ export class EditableView extends React.Component<EditableProps> {
if (!e.ctrlKey) {
if (this.props.SetValue(e.currentTarget.value, e.shiftKey)) {
this._editing = false;
+ this.props.isEditingCallback && this.props.isEditingCallback(false);
}
} else if (this.props.OnFillDown) {
this.props.OnFillDown(e.currentTarget.value);
this._editing = false;
+ this.props.isEditingCallback && this.props.isEditingCallback(false);
}
} else if (e.key === "Escape") {
e.stopPropagation();
this._editing = false;
+ this.props.isEditingCallback && this.props.isEditingCallback(false);
}
}
@@ -73,6 +95,7 @@ export class EditableView extends React.Component<EditableProps> {
e.nativeEvent.stopPropagation();
if (!this.props.onClick || !this.props.onClick(e)) {
this._editing = true;
+ this.props.isEditingCallback && this.props.isEditingCallback(true);
}
e.stopPropagation();
}
@@ -88,10 +111,26 @@ export class EditableView extends React.Component<EditableProps> {
render() {
if (this._editing) {
- return <input className="editableView-input" defaultValue={this.props.GetValue()} onKeyDown={this.onKeyDown} autoFocus
- onBlur={action(() => this._editing = false)} onPointerDown={this.stopPropagation} onClick={this.stopPropagation} onPointerUp={this.stopPropagation}
- style={{ display: this.props.display, fontSize: this.props.fontSize }} />;
+ return this.props.autosuggestProps
+ ? <Autosuggest
+ {...this.props.autosuggestProps.autosuggestProps}
+ inputProps={{
+ className: "editableView-input",
+ onKeyDown: this.onKeyDown,
+ autoFocus: true,
+ onBlur: action(() => this._editing = false),
+ onPointerDown: this.stopPropagation,
+ onClick: this.stopPropagation,
+ onPointerUp: this.stopPropagation,
+ value: this.props.autosuggestProps.value,
+ onChange: this.props.autosuggestProps.onChange
+ }}
+ />
+ : <input className="editableView-input" defaultValue={this.props.GetValue()} onKeyDown={this.onKeyDown} autoFocus
+ onBlur={action(() => { this._editing = false; this.props.isEditingCallback && this.props.isEditingCallback(false); })} onPointerDown={this.stopPropagation} onClick={this.stopPropagation} onPointerUp={this.stopPropagation}
+ style={{ display: this.props.display, fontSize: this.props.fontSize }} />;
} else {
+ if (this.props.autosuggestProps) this.props.autosuggestProps.resetValue();
return (
<div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`}
style={{ display: this.props.display, height: "auto", maxHeight: `${this.props.height}` }}
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 52292abbb..f5a6715e5 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,5 +1,5 @@
import { IconName, library } from '@fortawesome/fontawesome-svg-core';
-import { faArrowDown, faCloudUploadAlt, faArrowUp, faClone, faCheck, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faPortrait, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faThumbtack, faTree, faUndoAlt, faCat, faBolt } from '@fortawesome/free-solid-svg-icons';
+import { faArrowDown, faCaretUp, faLongArrowAltRight, faCloudUploadAlt, faArrowUp, faClone, faCheck, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faPortrait, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faThumbtack, faTree, faUndoAlt, faCat, faBolt } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, configure, observable, runInAction, reaction, trace } from 'mobx';
import { observer } from 'mobx-react';
@@ -38,6 +38,7 @@ import { PreviewCursor } from './PreviewCursor';
import { FilterBox } from './search/FilterBox';
import { CollectionTreeView } from './collections/CollectionTreeView';
import { ClientUtils } from '../util/ClientUtils';
+import { SchemaHeaderField, RandomPastel } from '../../new_fields/SchemaHeaderField';
@observer
export class MainView extends React.Component {
@@ -126,7 +127,9 @@ export class MainView extends React.Component {
library.add(faCut);
library.add(faCommentAlt);
library.add(faThumbtack);
+ library.add(faLongArrowAltRight);
library.add(faCheck);
+ library.add(faCaretUp);
library.add(faArrowDown);
library.add(faArrowUp);
library.add(faCloudUploadAlt);
@@ -372,7 +375,7 @@ export class MainView extends React.Component {
let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg";
// let addDockingNode = action(() => Docs.Create.StandardCollectionDockingDocument([{ doc: addColNode(), initialWidth: 200 }], { width: 200, height: 200, title: "a nested docking freeform collection" }));
- let addSchemaNode = action(() => Docs.Create.SchemaDocument(["title"], [], { width: 200, height: 200, title: "a schema collection" }));
+ let addSchemaNode = action(() => Docs.Create.SchemaDocument([new SchemaHeaderField("title")], [], { width: 200, height: 200, title: "a schema collection" }));
//let addTreeNode = action(() => Docs.TreeDocument([CurrentUserUtils.UserDocument], { width: 250, height: 400, title: "Library:" + CurrentUserUtils.email, dropAction: "alias" }));
// let addTreeNode = action(() => Docs.TreeDocument(this._northstarSchemas, { width: 250, height: 400, title: "northstar schemas", dropAction: "copy" }));
let addColNode = action(() => Docs.Create.FreeformDocument([], { width: this.pwidth * .7, height: this.pheight, title: "a freeform collection" }));
diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx
index c50e02f0d..652e0e91a 100644
--- a/src/client/views/MetadataEntryMenu.tsx
+++ b/src/client/views/MetadataEntryMenu.tsx
@@ -74,6 +74,7 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
this.userModified = e.target.value.trim() !== "";
}
+ @action
onValueKeyDown = async (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
e.stopPropagation();
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index 67112ae7c..c595a4c56 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -32,7 +32,7 @@ export interface CollectionRenderProps {
export interface CollectionViewProps extends FieldViewProps {
onContextMenu?: (e: React.MouseEvent) => void;
- children: (type: CollectionViewType, props: CollectionRenderProps) => JSX.Element | JSX.Element[] | null;
+ children: (type: CollectionViewType, props: CollectionRenderProps) => JSX.Element | JSX.Element[] | null | (JSX.Element | null)[];
className?: string;
contentRef?: React.Ref<HTMLDivElement>;
}
diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx
new file mode 100644
index 000000000..c91f1017b
--- /dev/null
+++ b/src/client/views/collections/CollectionSchemaCells.tsx
@@ -0,0 +1,289 @@
+import React = require("react");
+import { action, computed, observable, trace, untracked, toJS } from "mobx";
+import { observer } from "mobx-react";
+import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults, Column } from "react-table";
+import "react-table/react-table.css";
+import { emptyFunction, returnFalse, returnZero, returnOne } from "../../../Utils";
+import { Doc, DocListCast, DocListCastAsync, Field, Opt } from "../../../new_fields/Doc";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { SetupDrag, DragManager } from "../../util/DragManager";
+import { CompileScript } from "../../util/Scripting";
+import { Transform } from "../../util/Transform";
+import { COLLECTION_BORDER_WIDTH, MAX_ROW_HEIGHT } from '../globalCssVariables.scss';
+import '../DocumentDecorations.scss';
+import { EditableView } from "../EditableView";
+import { FieldView, FieldViewProps } from "../nodes/FieldView";
+import { CollectionPDFView } from "./CollectionPDFView";
+import "./CollectionSchemaView.scss";
+import { CollectionVideoView } from "./CollectionVideoView";
+import { CollectionView } from "./CollectionView";
+import { NumCast, StrCast, BoolCast, FieldValue, Cast } from "../../../new_fields/Types";
+import { Docs } from "../../documents/Documents";
+import { DocumentContentsView } from "../nodes/DocumentContentsView";
+import { SelectionManager } from "../../util/SelectionManager";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faExpand } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { SchemaHeaderField, RandomPastel } from "../../../new_fields/SchemaHeaderField";
+
+library.add(faExpand);
+
+export interface CellProps {
+ row: number;
+ col: number;
+ rowProps: CellInfo;
+ CollectionView: CollectionView | CollectionPDFView | CollectionVideoView;
+ ContainingCollection: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+ Document: Doc;
+ fieldKey: string;
+ renderDepth: number;
+ addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+ moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
+ isFocused: boolean;
+ changeFocusedCellByIndex: (row: number, col: number) => void;
+ setIsEditing: (isEditing: boolean) => void;
+ isEditable: boolean;
+ setPreviewDoc: (doc: Doc) => void;
+}
+
+@observer
+export class CollectionSchemaCell extends React.Component<CellProps> {
+ @observable protected _isEditing: boolean = false;
+ protected _focusRef = React.createRef<HTMLDivElement>();
+ protected _document = this.props.rowProps.original;
+ private _dropDisposer?: DragManager.DragDropDisposer;
+
+ componentDidMount() {
+ document.addEventListener("keydown", this.onKeyDown);
+
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener("keydown", this.onKeyDown);
+ }
+
+ @action
+ onKeyDown = (e: KeyboardEvent): void => {
+ if (this.props.isFocused && this.props.isEditable) {
+ document.removeEventListener("keydown", this.onKeyDown);
+ this._isEditing = true;
+ this.props.setIsEditing(true);
+ }
+ }
+
+ @action
+ isEditingCallback = (isEditing: boolean): void => {
+ document.addEventListener("keydown", this.onKeyDown);
+ this._isEditing = isEditing;
+ this.props.setIsEditing(isEditing);
+ this.props.changeFocusedCellByIndex(this.props.row, this.props.col);
+ }
+
+ @action
+ onPointerDown = (e: React.PointerEvent): void => {
+ this.props.changeFocusedCellByIndex(this.props.row, this.props.col);
+ }
+
+ applyToDoc = (doc: Doc, run: (args?: { [name: string]: any }) => any) => {
+ const res = run({ this: doc });
+ if (!res.success) return false;
+ doc[this.props.rowProps.column.id as string] = res.result;
+ return true;
+ }
+
+ private drop = (e: Event, de: DragManager.DropEvent) => {
+ if (de.data instanceof DragManager.DocumentDragData) {
+ let fieldKey = this.props.rowProps.column.id as string;
+ if (de.data.draggedDocuments.length === 1) {
+ this._document[fieldKey] = de.data.draggedDocuments[0];
+ }
+ else {
+ let coll = Docs.Create.SchemaDocument([new SchemaHeaderField("title")], de.data.draggedDocuments, {});
+ this._document[fieldKey] = coll;
+ }
+ e.stopPropagation();
+ }
+ }
+
+ private dropRef = (ele: HTMLElement) => {
+ this._dropDisposer && this._dropDisposer();
+ if (ele) {
+ this._dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
+ }
+ }
+
+ expandDoc = (e: React.PointerEvent) => {
+ let field = this.props.rowProps.original[this.props.rowProps.column.id as string];
+ let doc = FieldValue(Cast(field, Doc));
+
+ console.log("Expanding doc", StrCast(doc!.title));
+ this.props.setPreviewDoc(doc!);
+
+ // this.props.changeFocusedCellByIndex(this.props.row, this.props.col);
+
+ e.stopPropagation();
+ }
+
+ renderCellWithType(type: string | undefined) {
+ let dragRef: React.RefObject<HTMLDivElement> = React.createRef();
+
+ let props: FieldViewProps = {
+ Document: this.props.rowProps.original,
+ DataDoc: this.props.rowProps.original,
+ fieldKey: this.props.rowProps.column.id as string,
+ fieldExt: "",
+ ContainingCollectionView: this.props.CollectionView,
+ isSelected: returnFalse,
+ select: emptyFunction,
+ renderDepth: this.props.renderDepth + 1,
+ selectOnLoad: false,
+ ScreenToLocalTransform: Transform.Identity,
+ focus: emptyFunction,
+ active: returnFalse,
+ whenActiveChanged: emptyFunction,
+ PanelHeight: returnZero,
+ PanelWidth: returnZero,
+ addDocTab: this.props.addDocTab,
+ };
+
+ // let onItemDown = (e: React.PointerEvent) => {
+ // SetupDrag(this._focusRef, () => this._document[props.fieldKey] instanceof Doc ? this._document[props.fieldKey] : this._document,
+ // this._document[props.fieldKey] instanceof Doc ? (doc: Doc, target: Doc, addDoc: (newDoc: Doc) => any) => addDoc(doc) : this.props.moveDocument, this._document[props.fieldKey] instanceof Doc ? "alias" : this.props.Document.schemaDoc ? "copy" : undefined)(e);
+ // };
+ let onPointerEnter = (e: React.PointerEvent): void => {
+ if (e.buttons === 1 && SelectionManager.GetIsDragging() && (type === "document" || type === undefined)) {
+ dragRef!.current!.className = "collectionSchemaView-cellContainer doc-drag-over";
+ }
+ };
+ let onPointerLeave = (e: React.PointerEvent): void => {
+ dragRef!.current!.className = "collectionSchemaView-cellContainer";
+ };
+
+ let field = props.Document[props.fieldKey];
+ let contents: any = "incorrect type";
+ if (type === undefined) contents = <FieldView {...props} />;
+ if (type === "number") contents = typeof field === "number" ? NumCast(field) : "--" + typeof field + "--";
+ if (type === "string") contents = typeof field === "string" ? (StrCast(field) === "" ? "--" : StrCast(field)) : "--" + typeof field + "--";
+ if (type === "boolean") contents = typeof field === "boolean" ? (BoolCast(field) ? "true" : "false") : "--" + typeof field + "--";
+ if (type === "document") {
+ let doc = FieldValue(Cast(field, Doc));
+ contents = typeof field === "object" ? doc ? StrCast(doc.title) === "" ? "--" : StrCast(doc.title) : `--${typeof field}--` : `--${typeof field}--`;
+ }
+
+ let className = "collectionSchemaView-cellWrapper";
+ if (this._isEditing) className += " editing";
+ if (this.props.isFocused && this.props.isEditable) className += " focused";
+ if (this.props.isFocused && !this.props.isEditable) className += " inactive";
+
+ let doc = FieldValue(Cast(field, Doc));
+ if (type === "document") console.log("doc", typeof field);
+ let fieldIsDoc = (type === "document" && typeof field === "object") || (typeof field === "object" && doc);
+ let docExpander = (
+ <div className="collectionSchemaView-cellContents-docExpander" onPointerDown={this.expandDoc} >
+ <FontAwesomeIcon icon="expand" size="sm" />
+ </div>
+ );
+
+ return (
+ <div className="collectionSchemaView-cellContainer" ref={dragRef} onPointerDown={this.onPointerDown} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave}>
+ <div className={className} ref={this._focusRef} tabIndex={-1}>
+ <div className="collectionSchemaView-cellContents" ref={type === undefined || type === "document" ? this.dropRef : null} key={props.Document[Id]}>
+ <EditableView
+ editing={this._isEditing}
+ isEditingCallback={this.isEditingCallback}
+ display={"inline"}
+ contents={contents}
+ height={Number(MAX_ROW_HEIGHT)}
+ GetValue={() => {
+ let field = props.Document[props.fieldKey];
+ if (Field.IsField(field)) {
+ return Field.toScriptString(field);
+ }
+ return "";
+ }
+ }
+ SetValue={(value: string) => {
+ let script = CompileScript(value, { requiredType: type, addReturn: true, params: { this: Doc.name } });
+ if (!script.compiled) {
+ return false;
+ }
+ return this.applyToDoc(props.Document, script.run);
+ }}
+ OnFillDown={async (value: string) => {
+ let script = CompileScript(value, { requiredType: type, addReturn: true, params: { this: Doc.name } });
+ if (!script.compiled) {
+ return;
+ }
+ const run = script.run;
+ //TODO This should be able to be refactored to compile the script once
+ const val = await DocListCastAsync(this.props.Document[this.props.fieldKey]);
+ val && val.forEach(doc => this.applyToDoc(doc, run));
+ }} />
+ </div >
+ {fieldIsDoc ? docExpander : null}
+ </div>
+ </div>
+ );
+ }
+
+ render() {
+ return this.renderCellWithType(undefined);
+ }
+}
+
+@observer
+export class CollectionSchemaNumberCell extends CollectionSchemaCell {
+ render() {
+ return this.renderCellWithType("number");
+ }
+}
+
+@observer
+export class CollectionSchemaBooleanCell extends CollectionSchemaCell {
+ render() {
+ return this.renderCellWithType("boolean");
+ }
+}
+
+@observer
+export class CollectionSchemaStringCell extends CollectionSchemaCell {
+ render() {
+ return this.renderCellWithType("string");
+ }
+}
+
+@observer
+export class CollectionSchemaDocCell extends CollectionSchemaCell {
+ render() {
+ return this.renderCellWithType("document");
+ }
+}
+
+@observer
+export class CollectionSchemaCheckboxCell extends CollectionSchemaCell {
+ @observable private _isChecked: boolean = typeof this.props.rowProps.original[this.props.rowProps.column.id as string] === "boolean" ? BoolCast(this.props.rowProps.original[this.props.rowProps.column.id as string]) : false;
+
+ @action
+ toggleChecked = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this._isChecked = e.target.checked;
+ let script = CompileScript(e.target.checked.toString(), { requiredType: "boolean", addReturn: true, params: { this: Doc.name } });
+ if (script.compiled) {
+ this.applyToDoc(this._document, script.run);
+ }
+ }
+
+ render() {
+ let reference = React.createRef<HTMLDivElement>();
+ let onItemDown = (e: React.PointerEvent) => {
+ (!this.props.CollectionView.props.isSelected() ? undefined :
+ SetupDrag(reference, () => this._document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e));
+ };
+ return (
+ <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}>
+ <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} key={this._document[Id]} ref={reference}>
+ <input type="checkbox" checked={this._isChecked} onChange={this.toggleChecked} />
+ </div >
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/collections/CollectionSchemaHeaders.tsx b/src/client/views/collections/CollectionSchemaHeaders.tsx
new file mode 100644
index 000000000..9fc28eafa
--- /dev/null
+++ b/src/client/views/collections/CollectionSchemaHeaders.tsx
@@ -0,0 +1,299 @@
+import React = require("react");
+import { action, computed, observable, trace, untracked } from "mobx";
+import { observer } from "mobx-react";
+import "./CollectionSchemaView.scss";
+import { faPlus, faFont, faHashtag, faAlignJustify, faCheckSquare, faToggleOn } from '@fortawesome/free-solid-svg-icons';
+import { library, IconProp } from "@fortawesome/fontawesome-svg-core";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { Flyout, anchorPoints } from "../DocumentDecorations";
+import { ColumnType } from "./CollectionSchemaView";
+import { emptyFunction } from "../../../Utils";
+import { contains } from "typescript-collections/dist/lib/arrays";
+import { faFile } from "@fortawesome/free-regular-svg-icons";
+import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
+
+library.add(faPlus, faFont, faHashtag, faAlignJustify, faCheckSquare, faToggleOn, faFile);
+
+export interface HeaderProps {
+ keyValue: SchemaHeaderField;
+ possibleKeys: string[];
+ existingKeys: string[];
+ keyType: ColumnType;
+ typeConst: boolean;
+ onSelect: (oldKey: string, newKey: string, addnew: boolean) => void;
+ setIsEditing: (isEditing: boolean) => void;
+ deleteColumn: (column: string) => void;
+ setColumnType: (key: string, type: ColumnType) => void;
+ setColumnSort: (key: string, desc: boolean) => void;
+ removeColumnSort: (key: string) => void;
+}
+
+export class CollectionSchemaHeader extends React.Component<HeaderProps> {
+ render() {
+ let icon: IconProp = this.props.keyType === ColumnType.Number ? "hashtag" : this.props.keyType === ColumnType.String ? "font" :
+ this.props.keyType === ColumnType.Boolean ? "check-square" : this.props.keyType === ColumnType.Doc ? "file" : "align-justify";
+
+ return (
+ <div className="collectionSchemaView-header" style={{ background: this.props.keyValue.color }}>
+ <CollectionSchemaColumnMenu
+ keyValue={this.props.keyValue.heading}
+ possibleKeys={this.props.possibleKeys}
+ existingKeys={this.props.existingKeys}
+ keyType={this.props.keyType}
+ typeConst={this.props.typeConst}
+ menuButtonContent={<div><FontAwesomeIcon icon={icon} size="sm" />{this.props.keyValue.heading}</div>}
+ addNew={false}
+ onSelect={this.props.onSelect}
+ setIsEditing={this.props.setIsEditing}
+ deleteColumn={this.props.deleteColumn}
+ onlyShowOptions={false}
+ setColumnType={this.props.setColumnType}
+ setColumnSort={this.props.setColumnSort}
+ removeColumnSort={this.props.removeColumnSort}
+ />
+ </div>
+ );
+ }
+}
+
+
+export interface AddColumnHeaderProps {
+ createColumn: () => void;
+}
+
+@observer
+export class CollectionSchemaAddColumnHeader extends React.Component<AddColumnHeaderProps> {
+ render() {
+ return (
+ <button className="add-column" onClick={() => this.props.createColumn()}><FontAwesomeIcon icon="plus" size="sm" /></button>
+ );
+ }
+}
+
+
+
+export interface ColumnMenuProps {
+ keyValue: string;
+ possibleKeys: string[];
+ existingKeys: string[];
+ keyType: ColumnType;
+ typeConst: boolean;
+ menuButtonContent: JSX.Element;
+ addNew: boolean;
+ onSelect: (oldKey: string, newKey: string, addnew: boolean) => void;
+ setIsEditing: (isEditing: boolean) => void;
+ deleteColumn: (column: string) => void;
+ onlyShowOptions: boolean;
+ setColumnType: (key: string, type: ColumnType) => void;
+ setColumnSort: (key: string, desc: boolean) => void;
+ removeColumnSort: (key: string) => void;
+ anchorPoint?: any;
+}
+@observer
+export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> {
+ @observable private _isOpen: boolean = false;
+ @observable private _node: HTMLDivElement | null = null;
+
+ componentDidMount() {
+ document.addEventListener("pointerdown", this.detectClick);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener("pointerdown", this.detectClick);
+ }
+
+ detectClick = (e: PointerEvent): void => {
+ if (this._node && this._node.contains(e.target as Node)) {
+ } else {
+ this._isOpen = false;
+ this.props.setIsEditing(false);
+ }
+ }
+
+ @action
+ toggleIsOpen = (): void => {
+ this._isOpen = !this._isOpen;
+ this.props.setIsEditing(this._isOpen);
+ }
+
+ setColumnType = (oldKey: string, newKey: string, addnew: boolean) => {
+ let typeStr = newKey as keyof typeof ColumnType;
+ let type = ColumnType[typeStr];
+ this.props.setColumnType(this.props.keyValue, type);
+ }
+
+ @action
+ setNode = (node: HTMLDivElement): void => {
+ if (node) {
+ this._node = node;
+ }
+ }
+
+ renderTypes = () => {
+ if (this.props.typeConst) return <></>;
+ return (
+ <div className="collectionSchema-headerMenu-group">
+ <label>Column type:</label>
+ <div className="columnMenu-types">
+ <button title="Any" className={this.props.keyType === ColumnType.Any ? "active" : ""} onClick={() => this.props.setColumnType(this.props.keyValue, ColumnType.Any)}>
+ <FontAwesomeIcon icon={"align-justify"} size="sm" />
+ </button>
+ <button title="Number" className={this.props.keyType === ColumnType.Number ? "active" : ""} onClick={() => this.props.setColumnType(this.props.keyValue, ColumnType.Number)}>
+ <FontAwesomeIcon icon={"hashtag"} size="sm" />
+ </button>
+ <button title="String" className={this.props.keyType === ColumnType.String ? "active" : ""} onClick={() => this.props.setColumnType(this.props.keyValue, ColumnType.String)}>
+ <FontAwesomeIcon icon={"font"} size="sm" />
+ </button>
+ <button title="Checkbox" className={this.props.keyType === ColumnType.Boolean ? "active" : ""} onClick={() => this.props.setColumnType(this.props.keyValue, ColumnType.Boolean)}>
+ <FontAwesomeIcon icon={"check-square"} size="sm" />
+ </button>
+ <button title="Document" className={this.props.keyType === ColumnType.Doc ? "active" : ""} onClick={() => this.props.setColumnType(this.props.keyValue, ColumnType.Doc)}>
+ <FontAwesomeIcon icon={"file"} size="sm" />
+ </button>
+ </div>
+ </div>
+ );
+ }
+
+ renderSorting = () => {
+ return (
+ <div className="collectionSchema-headerMenu-group">
+ <label>Sort by:</label>
+ <div className="columnMenu-sort">
+ <div className="columnMenu-option" onClick={() => this.props.setColumnSort(this.props.keyValue, false)}>Sort ascending</div>
+ <div className="columnMenu-option" onClick={() => this.props.setColumnSort(this.props.keyValue, true)}>Sort descending</div>
+ <div className="columnMenu-option" onClick={() => this.props.removeColumnSort(this.props.keyValue)}>Clear sorting</div>
+ </div>
+ </div>
+ );
+ }
+
+ renderContent = () => {
+ return (
+ <div className="collectionSchema-header-menuOptions">
+ <label>Key:</label>
+ <div className="collectionSchema-headerMenu-group">
+ <KeysDropdown
+ keyValue={this.props.keyValue}
+ possibleKeys={this.props.possibleKeys}
+ existingKeys={this.props.existingKeys}
+ canAddNew={true}
+ addNew={this.props.addNew}
+ onSelect={this.props.onSelect}
+ setIsEditing={this.props.setIsEditing}
+ />
+ </div>
+ {this.props.onlyShowOptions ? <></> :
+ <>
+ {this.renderTypes()}
+ {this.renderSorting()}
+ <div className="collectionSchema-headerMenu-group">
+ <button onClick={() => this.props.deleteColumn(this.props.keyValue)}>Delete Column</button>
+ </div>
+ </>
+ }
+ </div>
+ );
+ }
+
+ render() {
+ return (
+ <div className="collectionSchema-header-menu" ref={this.setNode}>
+ <Flyout anchorPoint={this.props.anchorPoint ? this.props.anchorPoint : anchorPoints.TOP_CENTER} content={this.renderContent()}>
+ <div className="collectionSchema-header-toggler" onClick={() => this.toggleIsOpen()}>{this.props.menuButtonContent}</div>
+ </ Flyout >
+ </div>
+ );
+ }
+}
+
+
+interface KeysDropdownProps {
+ keyValue: string;
+ possibleKeys: string[];
+ existingKeys: string[];
+ canAddNew: boolean;
+ addNew: boolean;
+ onSelect: (oldKey: string, newKey: string, addnew: boolean) => void;
+ setIsEditing: (isEditing: boolean) => void;
+}
+@observer
+class KeysDropdown extends React.Component<KeysDropdownProps> {
+ @observable private _key: string = this.props.keyValue;
+ @observable private _searchTerm: string = "";
+ @observable private _isOpen: boolean = false;
+ @observable private _canClose: boolean = true;
+
+ @action setSearchTerm = (value: string): void => { this._searchTerm = value; };
+ @action setKey = (key: string): void => { this._key = key; };
+ @action setIsOpen = (isOpen: boolean): void => { this._isOpen = isOpen; };
+
+ @action
+ onSelect = (key: string): void => {
+ this.props.onSelect(this._key, key, this.props.addNew);
+ this.setKey(key);
+ this._isOpen = false;
+ this.props.setIsEditing(false);
+ }
+
+ onChange = (val: string): void => {
+ this.setSearchTerm(val);
+ }
+
+ @action
+ onFocus = (e: React.FocusEvent): void => {
+ this._isOpen = true;
+ this.props.setIsEditing(true);
+ }
+
+ @action
+ onBlur = (e: React.FocusEvent): void => {
+ if (this._canClose) {
+ this._isOpen = false;
+ this.props.setIsEditing(false);
+ }
+ }
+
+ @action
+ onPointerEnter = (e: React.PointerEvent): void => {
+ this._canClose = false;
+ }
+
+ @action
+ onPointerOut = (e: React.PointerEvent): void => {
+ this._canClose = true;
+ }
+
+ renderOptions = (): JSX.Element[] | JSX.Element => {
+ if (!this._isOpen) return <></>;
+
+ let keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1);
+ let exactFound = keyOptions.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1 ||
+ this.props.existingKeys.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1;
+
+ let options = keyOptions.map(key => {
+ return <div key={key} className="key-option" onClick={() => { this.onSelect(key); this.setSearchTerm(""); }}>{key}</div>;
+ });
+
+ // if search term does not already exist as a group type, give option to create new group type
+ if (!exactFound && this._searchTerm !== "" && this.props.canAddNew) {
+ options.push(<div key={""} className="key-option"
+ onClick={() => { this.onSelect(this._searchTerm); this.setSearchTerm(""); }}>
+ Create "{this._searchTerm}" key</div>);
+ }
+
+ return options;
+ }
+
+ render() {
+ return (
+ <div className="keys-dropdown">
+ <input className="keys-search" type="text" value={this._searchTerm} placeholder="Search for or create a new key"
+ onChange={e => this.onChange(e.target.value)} onFocus={this.onFocus} onBlur={this.onBlur}></input>
+ <div className="keys-options-wrapper" onPointerEnter={this.onPointerEnter} onPointerOut={this.onPointerOut}>
+ {this.renderOptions()}
+ </div>
+ </div >
+ );
+ }
+}
diff --git a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx
new file mode 100644
index 000000000..2349e42ca
--- /dev/null
+++ b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx
@@ -0,0 +1,231 @@
+import React = require("react");
+import { ReactTableDefaults, TableCellRenderer, ComponentPropsGetterR, ComponentPropsGetter0, RowInfo } from "react-table";
+import "./CollectionSchemaView.scss";
+import { Transform } from "../../util/Transform";
+import { Doc } from "../../../new_fields/Doc";
+import { DragManager, SetupDrag } from "../../util/DragManager";
+import { SelectionManager } from "../../util/SelectionManager";
+import { Cast, FieldValue, StrCast } from "../../../new_fields/Types";
+import { ContextMenu } from "../ContextMenu";
+import { action } from "mobx";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faGripVertical, faTrash } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { DocumentManager } from "../../util/DocumentManager";
+import { PastelSchemaPalette, SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
+
+library.add(faGripVertical, faTrash);
+
+export interface MovableColumnProps {
+ columnRenderer: TableCellRenderer;
+ columnValue: SchemaHeaderField;
+ allColumns: SchemaHeaderField[];
+ reorderColumns: (toMove: SchemaHeaderField, relativeTo: SchemaHeaderField, before: boolean, columns: SchemaHeaderField[]) => void;
+ ScreenToLocalTransform: () => Transform;
+}
+export class MovableColumn extends React.Component<MovableColumnProps> {
+ private _header?: React.RefObject<HTMLDivElement> = React.createRef();
+ private _colDropDisposer?: DragManager.DragDropDisposer;
+
+ onPointerEnter = (e: React.PointerEvent): void => {
+ if (e.buttons === 1 && SelectionManager.GetIsDragging()) {
+ this._header!.current!.className = "collectionSchema-col-wrapper";
+ document.addEventListener("pointermove", this.onDragMove, true);
+ }
+ }
+ onPointerLeave = (e: React.PointerEvent): void => {
+ this._header!.current!.className = "collectionSchema-col-wrapper";
+ document.removeEventListener("pointermove", this.onDragMove, true);
+ }
+ onDragMove = (e: PointerEvent): void => {
+ let x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY);
+ let rect = this._header!.current!.getBoundingClientRect();
+ let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left + ((rect.right - rect.left) / 2), rect.top);
+ let before = x[0] < bounds[0];
+ this._header!.current!.className = "collectionSchema-col-wrapper";
+ if (before) this._header!.current!.className += " col-before";
+ if (!before) this._header!.current!.className += " col-after";
+ e.stopPropagation();
+ }
+
+ createColDropTarget = (ele: HTMLDivElement) => {
+ this._colDropDisposer && this._colDropDisposer();
+ if (ele) {
+ this._colDropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.colDrop.bind(this) } });
+ }
+ }
+
+ colDrop = (e: Event, de: DragManager.DropEvent) => {
+ document.removeEventListener("pointermove", this.onDragMove, true);
+ let x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y);
+ let rect = this._header!.current!.getBoundingClientRect();
+ let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left + ((rect.right - rect.left) / 2), rect.top);
+ let before = x[0] < bounds[0];
+ if (de.data instanceof DragManager.ColumnDragData) {
+ this.props.reorderColumns(de.data.colKey, this.props.columnValue, before, this.props.allColumns);
+ return true;
+ }
+ return false;
+ }
+
+ setupDrag(ref: React.RefObject<HTMLElement>) {
+ let onRowMove = (e: PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+
+ document.removeEventListener("pointermove", onRowMove);
+ document.removeEventListener('pointerup', onRowUp);
+ let dragData = new DragManager.ColumnDragData(this.props.columnValue);
+ DragManager.StartColumnDrag(ref.current!, dragData, e.x, e.y);
+ };
+ let onRowUp = (): void => {
+ document.removeEventListener("pointermove", onRowMove);
+ document.removeEventListener('pointerup', onRowUp);
+ };
+ let onItemDown = (e: React.PointerEvent) => {
+ if (e.button === 0) {
+ e.stopPropagation();
+ document.addEventListener("pointermove", onRowMove);
+ document.addEventListener("pointerup", onRowUp);
+ }
+ };
+ return onItemDown;
+ }
+
+ // onColDrag = (e: React.DragEvent, ref: React.RefObject<HTMLDivElement>) => {
+ // this.setupDrag(reference);
+ // }
+
+
+ render() {
+ let reference = React.createRef<HTMLDivElement>();
+ let onItemDown = this.setupDrag(reference);
+
+ return (
+ <div className="collectionSchema-col" ref={this.createColDropTarget}>
+ <div className="collectionSchema-col-wrapper" ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
+ <div className="col-dragger" ref={reference} onPointerDown={onItemDown} >
+ {this.props.columnRenderer}
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+export interface MovableRowProps {
+ rowInfo: RowInfo;
+ ScreenToLocalTransform: () => Transform;
+ addDoc: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean;
+ removeDoc: (doc: Doc) => boolean;
+ rowFocused: boolean;
+ textWrapRow: (doc: Doc) => void;
+ rowWrapped: boolean;
+}
+
+export class MovableRow extends React.Component<MovableRowProps> {
+ private _header?: React.RefObject<HTMLDivElement> = React.createRef();
+ private _rowDropDisposer?: DragManager.DragDropDisposer;
+
+ onPointerEnter = (e: React.PointerEvent): void => {
+ if (e.buttons === 1 && SelectionManager.GetIsDragging()) {
+ this._header!.current!.className = "collectionSchema-row-wrapper";
+ document.addEventListener("pointermove", this.onDragMove, true);
+ }
+ }
+ onPointerLeave = (e: React.PointerEvent): void => {
+ this._header!.current!.className = "collectionSchema-row-wrapper";
+ document.removeEventListener("pointermove", this.onDragMove, true);
+ }
+ onDragMove = (e: PointerEvent): void => {
+ let x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY);
+ let rect = this._header!.current!.getBoundingClientRect();
+ let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2);
+ let before = x[1] < bounds[1];
+ this._header!.current!.className = "collectionSchema-row-wrapper";
+ if (before) this._header!.current!.className += " row-above";
+ if (!before) this._header!.current!.className += " row-below";
+ e.stopPropagation();
+ }
+
+ createRowDropTarget = (ele: HTMLDivElement) => {
+ this._rowDropDisposer && this._rowDropDisposer();
+ if (ele) {
+ this._rowDropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.rowDrop.bind(this) } });
+ }
+ }
+
+ rowDrop = (e: Event, de: DragManager.DropEvent) => {
+ const rowDoc = FieldValue(Cast(this.props.rowInfo.original, Doc));
+ if (!rowDoc) return false;
+
+ let x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y);
+ let rect = this._header!.current!.getBoundingClientRect();
+ let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2);
+ let before = x[1] < bounds[1];
+
+ if (de.data instanceof DragManager.DocumentDragData) {
+ e.stopPropagation();
+ if (de.data.draggedDocuments[0] === rowDoc) return true;
+ let addDocument = (doc: Doc) => this.props.addDoc(doc, rowDoc, before);
+ let movedDocs = de.data.draggedDocuments;
+ return (de.data.dropAction || de.data.userDropAction) ?
+ de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDoc(d, rowDoc, before) || added, false)
+ : (de.data.moveDocument) ?
+ movedDocs.reduce((added: boolean, d) => de.data.moveDocument(d, rowDoc, addDocument) || added, false)
+ : de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDoc(d, rowDoc, before), false);
+ }
+ return false;
+ }
+
+ onRowContextMenu = (e: React.MouseEvent): void => {
+ let description = this.props.rowWrapped ? "Unwrap text on row" : "Text wrap row";
+ ContextMenu.Instance.addItem({ description: description, event: () => this.props.textWrapRow(this.props.rowInfo.original) });
+ }
+
+ @action
+ move: DragManager.MoveFunction = (doc: Doc, target: Doc, addDoc) => {
+ let targetView = DocumentManager.Instance.getDocumentView(target);
+ if (targetView) {
+ let targetContainingColl = targetView.props.ContainingCollectionView; //.props.ContainingCollectionView.props.Document;
+ if (targetContainingColl) {
+ let targetContCollDoc = targetContainingColl.props.Document;
+ return doc !== target && doc !== targetContCollDoc && this.props.removeDoc(doc) && addDoc(doc);
+ }
+ }
+ return doc !== target && this.props.removeDoc(doc) && addDoc(doc);
+ }
+
+ render() {
+ const { children = null, rowInfo } = this.props;
+ if (!rowInfo) {
+ return <ReactTableDefaults.TrComponent>{children}</ReactTableDefaults.TrComponent>;
+ }
+
+ const { original } = rowInfo;
+ const doc = FieldValue(Cast(original, Doc));
+ if (!doc) return <></>;
+
+ let reference = React.createRef<HTMLDivElement>();
+ let onItemDown = SetupDrag(reference, () => doc, this.move);
+
+ let className = "collectionSchema-row";
+ if (this.props.rowFocused) className += " row-focused";
+ if (this.props.rowWrapped) className += " row-wrapped";
+ // if (!this.props.rowWrapped) className += " row-unwrapped";
+
+ return (
+ <div className={className} ref={this.createRowDropTarget} onContextMenu={this.onRowContextMenu}>
+ <div className="collectionSchema-row-wrapper" ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
+ <ReactTableDefaults.TrComponent>
+ <div className="row-dragger">
+ <div className="row-option" onClick={() => this.props.removeDoc(this.props.rowInfo.original)}><FontAwesomeIcon icon="trash" size="sm" /></div>
+ <div className="row-option" style={{ cursor: "grab" }} ref={reference} onPointerDown={onItemDown}><FontAwesomeIcon icon="grip-vertical" size="sm" /></div>
+ </div>
+ {children}
+ </ReactTableDefaults.TrComponent>
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss
index 186e006f3..e0de76247 100644
--- a/src/client/views/collections/CollectionSchemaView.scss
+++ b/src/client/views/collections/CollectionSchemaView.scss
@@ -1,26 +1,28 @@
@import "../globalCssVariables";
-
-
.collectionSchemaView-container {
border-width: $COLLECTION_BORDER_WIDTH;
border-color: $intermediate-color;
border-style: solid;
border-radius: $border-radius;
box-sizing: border-box;
- position: absolute;
+ // position: absolute;
width: 100%;
- height: 100%;
+ height: calc(100% - 50px);
+ // overflow: hidden;
+ // overflow-x: scroll;
+ // border: none;
overflow: hidden;
+ transition: top 0.5s;
- .collectionSchemaView-cellContents {
- height: $MAX_ROW_HEIGHT;
+ // .collectionSchemaView-cellContents {
+ // height: $MAX_ROW_HEIGHT;
- img {
- width: auto;
- max-height: $MAX_ROW_HEIGHT;
- }
- }
+ // img {
+ // width: auto;
+ // max-height: $MAX_ROW_HEIGHT;
+ // }
+ // }
.collectionSchemaView-previewRegion {
position: relative;
@@ -47,329 +49,433 @@
}
}
- .collectionSchemaView-previewHandle {
- position: absolute;
- height: 15px;
- width: 15px;
- z-index: 20;
- right: 0;
- top: 20px;
- background: Black;
- }
-
.collectionSchemaView-dividerDragger {
position: relative;
- background: black;
float: left;
- height: 37px;
+ height: 100%;
width: 20px;
z-index: 20;
right: 0;
top: 0;
- background: $main-accent;
- }
-
- .collectionSchemaView-columnsHandle {
- position: absolute;
- height: 37px;
- width: 20px;
- z-index: 20;
- left: 0;
- bottom: 0;
- background: $main-accent;
+ background: gray;
+ cursor: col-resize;
+ // background: $main-accent;
+ // box-sizing: border-box;
+ // border-left: 1px solid $intermediate-color;
+ // border-right: 1px solid $intermediate-color;
}
+}
- .collectionSchemaView-colDividerDragger {
- position: relative;
- box-sizing: border-box;
- border-top: 1px solid $intermediate-color;
- border-bottom: 1px solid $intermediate-color;
- float: top;
- width: 100%;
- }
+.ReactTable {
+ width: 100%;
+ height: 100%;
+ background: white;
+ box-sizing: border-box;
+ border: none !important;
- .collectionSchemaView-dividerDragger {
- position: relative;
- box-sizing: border-box;
- border-left: 1px solid $intermediate-color;
- border-right: 1px solid $intermediate-color;
- float: left;
+ .rt-table {
+ overflow-y: auto;
+ overflow-x: auto;
height: 100%;
+ display: -webkit-inline-box;
+ direction: ltr;
}
- .collectionSchemaView-tableContainer {
- position: relative;
- float: left;
- height: 100%;
- }
+ .rt-thead {
+ width: calc(100% - 50px);
+ margin-left: 50px;
+
+ &.-header {
+ // background: $intermediate-color;
+ // color: $light-color;
+ font-size: 12px;
+ height: 30px;
+ // border: 1px solid $intermediate-color;
+ box-shadow: none;
+ // width: calc(100% - 30px);
+ // margin-right: -30px;
+ }
- .ReactTable {
- // position: absolute; // display: inline-block;
- // overflow: auto;
- width: 100%;
- height: 100%;
- background: $light-color;
- box-sizing: border-box;
- border: none !important;
+ .rt-resizable-header {
+ padding: 0;
+ height: 30px;
- .rt-table {
- overflow-y: auto;
- overflow-x: auto;
- height: 100%;
- display: -webkit-inline-box;
- direction: ltr; // direction:rtl;
- // display:block;
+ &:last-child {
+ overflow: visible;
+ }
}
- .rt-tbody {
- //direction: ltr;
- direction: rtl;
+ .rt-resizable-header-content {
+ height: 100%;
+ overflow: visible;
}
- .rt-tr-group {
- direction: ltr;
- max-height: $MAX_ROW_HEIGHT;
+ .rt-th {
+ padding: 0;
+ border: solid lightgray;
+ border-width: 0 1px;
}
+ }
- .rt-td {
- border-width: 1px;
- border-right-color: $intermediate-color;
-
- .imageBox-cont {
- position: relative;
- max-height: 100%;
- }
+ .rt-th {
+ // max-height: $MAX_ROW_HEIGHT;
+ font-size: 13px;
+ text-align: center;
+ background-color: $light-color-secondary;
+
+ &:last-child {
+ overflow: visible;
+ }
+ }
- .imageBox-cont img {
- object-fit: contain;
- max-width: 100%;
- height: 100%;
- }
+ .rt-tbody {
+ direction: rtl;
+ overflow: visible;
+ }
- .videoBox-cont {
- object-fit: contain;
- width: auto;
- height: 100%;
- }
- }
+ .rt-tr-group {
+ direction: ltr;
+ flex: 0 1 auto;
+ min-height: 30px;
+ border: 0 !important;
+ // border: solid lightgray;
+ // border-width: 1px 0;
+ // border-left: 1px solid lightgray;
+ // max-height: $MAX_ROW_HEIGHT;
+ // for sub comp
+
+ // &:nth-child(even) {
+ // background-color: $light-color;
+ // }
+
+ // &:nth-child(odd) {
+ // background-color: $light-color-secondary;
+ // }
+
+ // &:first-child {
+ // border-top: 1px solid $light-color-secondary !important;
+ // }
+ // &:last-child {
+ // border-bottom: 1px solid $light-color-secondary !important;
+ // }
}
- .ReactTable .rt-thead.-header {
- background: $intermediate-color;
- color: $light-color;
- // text-transform: uppercase;
- letter-spacing: 2px;
- font-size: 12px;
- height: 30px;
- padding-top: 4px;
+ .rt-tr {
+ width: 100%;
+ min-height: 30px;
+ // height: $MAX_ROW_HEIGHT;
}
- .ReactTable .rt-th,
- .ReactTable .rt-td {
- max-height: $MAX_ROW_HEIGHT;
- padding: 3px 7px;
+ .rt-td {
+ // border: 1px solid $light-color-secondary !important;
+ // border-width: 0 1px;
+ // border-width: 1px;
+ // border-right-color: $intermediate-color;
+ // max-height: $MAX_ROW_HEIGHT;
+ padding: 0;
font-size: 13px;
text-align: center;
- }
+
+ // white-space: normal;
- .ReactTable .rt-tbody .rt-tr-group:last-child {
- border-bottom: $intermediate-color;
- border-bottom-style: solid;
- border-bottom-width: 1;
- }
+ .imageBox-cont {
+ position: relative;
+ max-height: 100%;
+ }
- .documentView-node-topmost {
- text-align: left;
- transform-origin: center top;
- display: inline-block;
- }
+ .imageBox-cont img {
+ object-fit: contain;
+ max-width: 100%;
+ height: 100%;
+ }
- .documentView-node:first-child {
- background: $light-color;
+ .videoBox-cont {
+ object-fit: contain;
+ width: auto;
+ height: 100%;
+ }
}
}
-//options menu styling
-#schemaOptionsMenuBtn {
- position: absolute;
- height: 20px;
- width: 20px;
- border-radius: 50%;
- z-index: 21;
- right: 4px;
- top: 4px;
- pointer-events: auto;
- background-color: black;
+.documentView-node-topmost {
+ text-align: left;
+ transform-origin: center top;
display: inline-block;
- padding: 0px;
- font-size: 100%;
}
-ul {
- list-style-type: disc;
+.documentView-node:first-child {
+ background: $light-color;
}
-#schema-options-header {
- text-align: center;
- padding: 0px;
- margin: 0px;
-}
+.collectionSchema-col{
+ height: 100%;
-.schema-options-subHeader {
- color: $intermediate-color;
- margin-bottom: 5px;
-}
+ .collectionSchema-col-wrapper {
+ &.col-before {
+ border-left: 2px solid red;
+ }
+ &.col-after {
+ border-right: 2px solid red;
+ }
+ }
+}
-#schemaOptionsMenuBtn:hover {
- transform: scale(1.15);
-}
-#preview-schema-checkbox-div {
- margin-left: 20px;
- font-size: 12px;
+.collectionSchemaView-header {
+ height: 100%;
+ color: gray;
+
+ .collectionSchema-header-menu {
+ height: 100%;
+
+ .collectionSchema-header-toggler {
+ width: 100%;
+ height: 100%;
+ padding: 4px;
+ letter-spacing: 2px;
+ text-transform: uppercase;
+
+ svg {
+ margin-right: 4px;
+ }
+ }
+
+ // div[class*="css"] {
+ // width: 100%;
+ // height: 100%;
+ // }
+ }
}
-#options-flyout-div {
- text-align: left;
- padding: 0px;
- z-index: 100;
- font-family: $sans-serif;
- padding-left: 5px;
+button.add-column {
+ width: 28px;
}
-#schema-col-checklist {
- overflow: scroll;
+.collectionSchema-header-menuOptions {
+ color: black;
+ width: 175px;
text-align: left;
- //background-color: $light-color-secondary;
- line-height: 25px;
- max-height: 175px;
- font-family: $sans-serif;
- font-size: 12px;
-}
+ .collectionSchema-headerMenu-group {
+ margin-bottom: 10px;
+ }
+
+ label {
+ color: $main-accent;
+ font-weight: normal;
+ }
-.Resizer {
- box-sizing: border-box;
- background: #000;
- opacity: 0.5;
- z-index: 1;
- background-clip: padding-box;
-
- &.horizontal {
- height: 11px;
- margin: -5px 0;
- border-top: 5px solid rgba(255, 255, 255, 0);
- border-bottom: 5px solid rgba(255, 255, 255, 0);
- cursor: row-resize;
+ input {
+ color: black;
width: 100%;
+ }
+
+ .keys-dropdown {
+ position: relative;
+ max-width: 175px;
- &:hover {
- border-top: 5px solid rgba(0, 0, 0, 0.5);
- border-bottom: 5px solid rgba(0, 0, 0, 0.5);
+ .keys-options-wrapper {
+ width: 100%;
+ max-height: 150px;
+ overflow-y: scroll;
+ position: absolute;
+ top: 20px;
+
+ .key-option {
+ background-color: $light-color;
+ border: 1px solid $light-color-secondary;
+ padding: 2px 3px;
+
+ &:not(:last-child) {
+ border-top: 0;
+ }
+
+ &:hover {
+ background-color: $light-color-secondary;
+ }
+ }
}
}
- &.vertical {
- width: 11px;
- margin: 0 -5px;
- border-left: 5px solid rgba(255, 255, 255, 0);
- border-right: 5px solid rgba(255, 255, 255, 0);
- cursor: col-resize;
+ .columnMenu-types {
+ display: flex;
+ justify-content: space-between;
- &:hover {
- border-left: 5px solid rgba(0, 0, 0, 0.5);
- border-right: 5px solid rgba(0, 0, 0, 0.5);
+ button {
+ border-radius: 20px;
}
}
+}
- &:hover {
- -webkit-transition: all 2s ease;
- transition: all 2s ease;
+.collectionSchema-row {
+ // height: $MAX_ROW_HEIGHT;
+ height: 100%;
+ background-color: white;
+
+ &.row-focused {
+ background-color: rgb(255, 246, 246);//$light-color-secondary;
}
-}
-.vertical {
- section {
- width: 100vh;
- height: 100vh;
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
- -webkit-box-orient: vertical;
- -webkit-box-direction: normal;
- -webkit-flex-direction: column;
- -ms-flex-direction: column;
- flex-direction: column;
+ &.row-wrapped {
+ white-space: normal;
}
- header {
- padding: 1rem;
- background: #eee;
+ .row-dragger {
+ display: flex;
+ justify-content: space-around;
+ // height: $MAX_ROW_HEIGHT;
+ flex: 50 0 auto;
+ width: 50px;
+ max-width: 50px;
+ height: 100%;
+ min-height: 30px;
+ // padding: 5px 5px 5px 0;
+ color: lightgray;
+ background-color: white;
+ transition: color 0.1s ease;
+
+ // &:hover {
+ // color: lightgray;
+ // }
+
+ .row-option {
+ // padding: 5px;
+ cursor: pointer;
+ transition: color 0.1s ease;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+
+ &:hover {
+ color: gray;
+ }
+ }
}
- footer {
- padding: 1rem;
- background: #eee;
+ .collectionSchema-row-wrapper {
+ // max-height: $MAX_ROW_HEIGHT;
+
+ &.row-above {
+ border-top: 1px solid red;
+ }
+ &.row-below {
+ border-bottom: 1px solid red;
+ }
+ &.row-inside {
+ border: 1px solid red;
+ }
+
+ .row-dragging {
+ background-color: blue;
+ }
}
}
-.horizontal {
- section {
- width: 100vh;
- height: 100vh;
- display: flex;
- flex-direction: column;
+.collectionSchemaView-cellContainer {
+ width: 100%;
+ height: 100%;
+}
+
+.collectionSchemaView-cellWrapper {
+ height: 100%;
+ padding: 4px;
+ position: relative;
+
+ &:focus {
+ outline: none;
}
- header {
- padding: 1rem;
- background: #eee;
+ &.focused {
+ // background-color: yellowgreen;
+ // border: 2px solid yellowgreen;
+
+ input {
+ outline: 0;
+ border: none;
+ background-color: yellow;
+ }
+
+ &.inactive {
+ // border: 2px solid rgba(255, 255, 0, 0.4);
+ border: none;
+ }
}
- footer {
- padding: 1rem;
- background: #eee;
+ p {
+ width: 100%;
+ height: 100%;
+ // word-wrap: break-word;
+ }
+
+ &:hover .collectionSchemaView-cellContents-docExpander {
+ display: block;
}
}
-.parent {
- width: 100%;
- height: 100%;
- -webkit-box-flex: 1;
- -webkit-flex: 1;
- -ms-flex: 1;
- flex: 1;
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
+.collectionSchemaView-cellContents-docExpander {
+ height: 30px;
+ width: 30px;
+ display: none;
+ position: absolute;
+ top: 0;
+ right: 0;
+ background-color: lightgray;
+}
+
+.doc-drag-over {
+ background-color: red;
+}
+
+.collectionSchemaView-toolbar {
+ height: 30px;
display: flex;
- -webkit-box-orient: vertical;
- -webkit-box-direction: normal;
- -webkit-flex-direction: column;
- -ms-flex-direction: column;
- flex-direction: column;
+ justify-content: flex-end;
+ padding: 0 10px;
+
+ border-bottom: 2px solid gray;
+ // margin-bottom: 10px;
+
+ .collectionSchemaView-toolbar-item {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ }
}
-.header {
- background: #aaa;
- height: 3rem;
- line-height: 3rem;
+#preview-schema-checkbox-div {
+ margin-left: 20px;
+ font-size: 12px;
}
-.wrapper {
- background: #ffa;
- margin: 5rem;
- -webkit-box-flex: 1;
- -webkit-flex: 1;
- -ms-flex: 1;
- flex: 1;
+.collectionSchemaView-table {
+ width: calc(100% - 7px);
}
-.-even {
- background: $light-color !important;
+.sub {
+ padding: 10px 30px;
+ // padding-left: 80px;
+ background-color: rgb(252, 252, 252);
+ width: calc(100% - 50px);
+ margin-left: 50px;
+
+ .rt-table {
+ overflow-x: hidden; // todo; this shouldnt be like this :((
+ overflow-y: visible;
+ } // TODO fix
+
+ .row-dragger {
+ background-color: rgb(252, 252, 252);
+ }
+
+ .rt-table {
+ background-color: rgb(252, 252, 252);
+ }
+
+ .collectionSchemaView-table {
+ width: 100%;
+ }
}
-.-odd {
- background: $light-color-secondary !important;
+.collectionSchemaView-expander {
+ height: 100%;
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index a57d3c81a..d504f9799 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -1,18 +1,18 @@
import React = require("react");
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faCog, faPlus, faTable } from '@fortawesome/free-solid-svg-icons';
+import { faCog, faPlus, faTable, faSortUp, faSortDown } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, observable, trace, untracked } from "mobx";
import { observer } from "mobx-react";
-import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table";
+import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults, TableCellRenderer, Column, RowInfo } from "react-table";
import "react-table/react-table.css";
import { emptyFunction, returnFalse, returnZero, returnOne } from "../../../Utils";
-import { Doc, DocListCast, DocListCastAsync, Field } from "../../../new_fields/Doc";
+import { Doc, DocListCast, DocListCastAsync, Field, FieldResult, Opt } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
import { List } from "../../../new_fields/List";
import { listSpec } from "../../../new_fields/Schema";
+import { Docs, DocumentOptions } from "../../documents/Documents";
import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
-import { Docs } from "../../documents/Documents";
import { Gateway } from "../../northstar/manager/Gateway";
import { SetupDrag, DragManager } from "../../util/DragManager";
import { CompileScript, ts, Transformer } from "../../util/Scripting";
@@ -30,29 +30,36 @@ import { CollectionSubView } from "./CollectionSubView";
import { CollectionVideoView } from "./CollectionVideoView";
import { CollectionView } from "./CollectionView";
import { undoBatch } from "../../util/UndoManager";
+import { timesSeries } from "async";
+import { CollectionSchemaHeader, CollectionSchemaAddColumnHeader } from "./CollectionSchemaHeaders";
+import { CellProps, CollectionSchemaCell, CollectionSchemaNumberCell, CollectionSchemaStringCell, CollectionSchemaBooleanCell, CollectionSchemaCheckboxCell, CollectionSchemaDocCell } from "./CollectionSchemaCells";
+import { MovableColumn, MovableRow } from "./CollectionSchemaMovableTableHOC";
+import { SelectionManager } from "../../util/SelectionManager";
+import { DocumentManager } from "../../util/DocumentManager";
+import { ImageBox } from "../nodes/ImageBox";
import { ComputedField } from "../../../new_fields/ScriptField";
+import { SchemaHeaderField, RandomPastel } from "../../../new_fields/SchemaHeaderField";
-library.add(faCog);
-library.add(faPlus, faTable);
+library.add(faCog, faPlus, faSortUp, faSortDown);
+library.add(faTable);
// bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657
-
-@observer
-class KeyToggle extends React.Component<{ keyName: string, checked: boolean, toggle: (key: string) => void }> {
- constructor(props: any) {
- super(props);
- }
-
- render() {
- return (
- <div key={this.props.keyName}>
- <input type="checkbox" checked={this.props.checked} onChange={() => this.props.toggle(this.props.keyName)} />
- {this.props.keyName}
- </div>
- );
- }
+export enum ColumnType {
+ Any,
+ Number,
+ String,
+ Boolean,
+ Doc,
+ // Checkbox
}
+// this map should be used for keys that should have a const type of value
+const columnTypes: Map<string, ColumnType> = new Map([
+ ["title", ColumnType.String],
+ ["x", ColumnType.Number], ["y", ColumnType.Number], ["width", ColumnType.Number], ["height", ColumnType.Number],
+ ["nativeWidth", ColumnType.Number], ["nativeHeight", ColumnType.Number], ["isPrototype", ColumnType.Boolean],
+ ["page", ColumnType.Number], ["curPage", ColumnType.Number], ["libraryBrush", ColumnType.Boolean], ["zIndex", ColumnType.Number]
+]);
@observer
export class CollectionSchemaView extends CollectionSubView(doc => doc) {
@@ -60,182 +67,368 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
private _startPreviewWidth = 0;
private DIVIDER_WIDTH = 4;
- @observable _columns: Array<string> = ["title", "data", "author"];
- @observable _selectedIndex = 0;
- @observable _columnsPercentage = 0;
- @observable _keys: string[] = [];
- @observable _newKeyName: string = "";
@observable previewScript: string = "";
+ @observable previewDoc: Doc | undefined = undefined;
+ @observable private _node: HTMLDivElement | null = null;
+ @observable private _focusedTable: Doc = this.props.Document;
+ @computed get chromeCollapsed() { return this.props.chromeCollapsed; }
@computed get previewWidth() { return () => NumCast(this.props.Document.schemaPreviewWidth); }
@computed get previewHeight() { return () => this.props.PanelHeight() - 2 * this.borderWidth; }
@computed get tableWidth() { return this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH - this.previewWidth(); }
- @computed get columns() { return Cast(this.props.Document.schemaColumns, listSpec("string"), []); }
@computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); }
- @computed get tableColumns() {
- return this.columns.map(col => {
- const ref = React.createRef<HTMLParagraphElement>();
- return {
- Header: <p ref={ref} onPointerDown={SetupDrag(ref, () => this.onHeaderDrag(col), undefined, "copy")}>{col}</p>,
- accessor: (doc: Doc) => doc ? doc[col] : 0,
- id: col
- };
- });
+
+ private createTarget = (ele: HTMLDivElement) => {
+ this._mainCont = ele;
+ super.CreateDropTarget(ele);
}
- onHeaderDrag = (columnName: string) => {
- let schemaDoc = Cast(this.props.Document.schemaDoc, Doc);
- if (schemaDoc instanceof Doc) {
- let columnDocs = DocListCast(schemaDoc.data);
- if (columnDocs) {
- let ddoc = columnDocs.find(doc => doc.title === columnName);
- if (ddoc) {
- return ddoc;
- }
- }
+ // detectClick = (e: PointerEvent): void => {
+ // if (this._node && this._node.contains(e.target as Node)) {
+ // } else {
+ // this._isOpen = false;
+ // this.props.setIsEditing(false);
+ // }
+ // }
+
+ isFocused = (doc: Doc): boolean => {
+ if (!this.props.isSelected()) return false;
+ return doc === this._focusedTable;
+ }
+
+ @action
+ setFocused = (doc: Doc): void => {
+ this._focusedTable = doc;
+ }
+
+ @action
+ setPreviewDoc = (doc: Doc): void => {
+ this.previewDoc = doc;
+ }
+
+ //toggles preview side-panel of schema
+ @action
+ toggleExpander = () => {
+ this.props.Document.schemaPreviewWidth = this.previewWidth() === 0 ? Math.min(this.tableWidth / 3, 200) : 0;
+ }
+
+ onDividerDown = (e: React.PointerEvent) => {
+ this._startPreviewWidth = this.previewWidth();
+ e.stopPropagation();
+ e.preventDefault();
+ document.addEventListener("pointermove", this.onDividerMove);
+ document.addEventListener('pointerup', this.onDividerUp);
+ }
+ @action
+ onDividerMove = (e: PointerEvent): void => {
+ let nativeWidth = this._mainCont!.getBoundingClientRect();
+ this.props.Document.schemaPreviewWidth = Math.min(nativeWidth.right - nativeWidth.left - 40,
+ this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0]);
+ }
+ @action
+ onDividerUp = (e: PointerEvent): void => {
+ document.removeEventListener("pointermove", this.onDividerMove);
+ document.removeEventListener('pointerup', this.onDividerUp);
+ if (this._startPreviewWidth === this.previewWidth()) {
+ this.toggleExpander();
}
- return this.props.Document;
}
- getField(row: number, col?: number) {
- const docs = DocListCast(this.props.Document[this.props.fieldKey]);
- row = row % docs.length;
- while (row < 0) row += docs.length;
- const columns = this.columns;
- const doc = docs[row];
- if (col === undefined) {
- return doc;
+ onPointerDown = (e: React.PointerEvent): void => {
+ if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) {
+ if (this.props.isSelected()) e.stopPropagation();
+ else {
+ this.props.select(false);
+ }
}
- if (col >= 0 && col < columns.length) {
- const column = this.columns[col];
- return doc[column];
+ }
+
+ onWheel = (e: React.WheelEvent): void => {
+ if (this.props.active()) {
+ e.stopPropagation();
}
- return undefined;
}
- createTransformer = (row: number, col: number): Transformer => {
- const self = this;
- const captures: { [name: string]: Field } = {};
-
- const transformer: ts.TransformerFactory<ts.SourceFile> = context => {
- return root => {
- function visit(node: ts.Node) {
- node = ts.visitEachChild(node, visit, context);
- if (ts.isIdentifier(node)) {
- const isntPropAccess = !ts.isPropertyAccessExpression(node.parent) || node.parent.expression === node;
- const isntPropAssign = !ts.isPropertyAssignment(node.parent) || node.parent.name !== node;
- if (isntPropAccess && isntPropAssign) {
- if (node.text === "$r") {
- return ts.createNumericLiteral(row.toString());
- } else if (node.text === "$c") {
- return ts.createNumericLiteral(col.toString());
- } else if (node.text === "$") {
- if (ts.isCallExpression(node.parent)) {
- captures.doc = self.props.Document;
- captures.key = self.props.fieldKey;
- }
- }
- }
- }
+ @computed
+ get previewDocument(): Doc | undefined {
+ let selected = this.previewDoc;
+ let pdc = selected ? (this.previewScript && this.previewScript !== "this" ? FieldValue(Cast(selected[this.previewScript], Doc)) : selected) : undefined;
+ return pdc;
+ }
- return node;
- }
- return ts.visitNode(root, visit);
- };
- };
+ getPreviewTransform = (): Transform => {
+ return this.props.ScreenToLocalTransform().translate(- this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth, - this.borderWidth);
+ }
- const getVars = () => {
- return { capturedVariables: captures };
- };
+ @computed
+ get dividerDragger() {
+ return this.previewWidth() === 0 ? (null) :
+ <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} />;
+ }
+
+ @computed
+ get previewPanel() {
+ let layoutDoc = this.previewDocument ? Doc.expandTemplateLayout(this.previewDocument, this.props.DataDoc) : undefined;
+ return <div ref={this.createTarget}>
+ <CollectionSchemaPreview
+ Document={layoutDoc}
+ DataDocument={this.previewDocument !== this.props.DataDoc ? this.props.DataDoc : undefined}
+ childDocs={this.childDocs}
+ renderDepth={this.props.renderDepth}
+ width={this.previewWidth}
+ height={this.previewHeight}
+ getTransform={this.getPreviewTransform}
+ CollectionView={this.props.CollectionView}
+ moveDocument={this.props.moveDocument}
+ addDocument={this.props.addDocument}
+ removeDocument={this.props.removeDocument}
+ active={this.props.active}
+ whenActiveChanged={this.props.whenActiveChanged}
+ addDocTab={this.props.addDocTab}
+ setPreviewScript={this.setPreviewScript}
+ previewScript={this.previewScript}
+ />
+ </div>;
+ }
+ @action
+ setPreviewScript = (script: string) => {
+ this.previewScript = script;
+ }
- return { transformer, getVars };
+ @computed
+ get schemaTable() {
+ return (
+ <SchemaTable
+ Document={this.props.Document} // child doc
+ PanelHeight={this.props.PanelHeight}
+ PanelWidth={this.props.PanelWidth}
+ // childDocs={this.childDocs}
+ CollectionView={this.props.CollectionView}
+ ContainingCollectionView={this.props.ContainingCollectionView}
+ fieldKey={this.props.fieldKey} // might just be this.
+ renderDepth={this.props.renderDepth}
+ moveDocument={this.props.moveDocument}
+ ScreenToLocalTransform={this.props.ScreenToLocalTransform}
+ active={this.props.active}
+ onDrop={this.onDrop}
+ addDocTab={this.props.addDocTab}
+ isSelected={this.props.isSelected}
+ isFocused={this.isFocused}
+ setFocused={this.setFocused}
+ setPreviewDoc={this.setPreviewDoc}
+ deleteDocument={this.props.removeDocument}
+ dataDoc={this.props.DataDoc}
+ />
+ );
+ }
+
+ @computed
+ public get schemaToolbar() {
+ return (
+ <div className="collectionSchemaView-toolbar">
+ <div className="collectionSchemaView-toolbar-item">
+ <div id="preview-schema-checkbox-div"><input type="checkbox" key={"Show Preview"} checked={this.previewWidth() !== 0} onChange={this.toggleExpander} />Show Preview</div>
+ </div>
+ </div>
+ );
+ }
+
+ render() {
+ // if (SelectionManager.SelectedDocuments().length > 0) console.log(StrCast(SelectionManager.SelectedDocuments()[0].Document.title));
+ // if (DocumentManager.Instance.getDocumentView(this.props.Document)) console.log(StrCast(this.props.Document.title), SelectionManager.IsSelected(DocumentManager.Instance.getDocumentView(this.props.Document)!))
+ return (
+ <div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} onWheel={this.onWheel}
+ onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createTarget}>
+ {this.schemaTable}
+ {this.dividerDragger}
+ {!this.previewWidth() ? (null) : this.previewPanel}
+ </div>
+ );
}
+}
+
+export interface SchemaTableProps {
+ Document: Doc; // child doc
+ dataDoc?: Doc;
+ PanelHeight: () => number;
+ PanelWidth: () => number;
+ // childDocs: Doc[];
+ CollectionView: CollectionView | CollectionPDFView | CollectionVideoView;
+ ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+ fieldKey: string;
+ renderDepth: number;
+ deleteDocument: (document: Doc) => boolean;
+ moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
+ ScreenToLocalTransform: () => Transform;
+ // CreateDropTarget: (ele: HTMLDivElement)=> void; // super createdriotarget
+ active: () => boolean;
+ onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void;
+ addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+ isSelected: () => boolean;
+ isFocused: (document: Doc) => boolean;
+ setFocused: (document: Doc) => void;
+ setPreviewDoc: (document: Doc) => void;
+}
+
+@observer
+export class SchemaTable extends React.Component<SchemaTableProps> {
+ // private _mainCont?: HTMLDivElement;
+ private DIVIDER_WIDTH = 4;
+
+ @observable _headerIsEditing: boolean = false;
+ @observable _cellIsEditing: boolean = false;
+ @observable _focusedCell: { row: number, col: number } = { row: 0, col: 0 };
+ @observable _sortedColumns: Map<string, { id: string, desc: boolean }> = new Map();
+ @observable _openCollections: Array<string> = [];
+ @observable _textWrappedRows: Array<string> = [];
+ @observable private _node: HTMLDivElement | null = null;
- setComputed(script: string, doc: Doc, field: string, row: number, col: number): boolean {
- script =
- `const $ = (row:number, col?:number) => {
- if(col === undefined) {
- return (doc as any)[key][row + ${row}];
+ @computed get previewWidth() { return () => NumCast(this.props.Document.schemaPreviewWidth); }
+ @computed get previewHeight() { return () => this.props.PanelHeight() - 2 * this.borderWidth; }
+ @computed get tableWidth() { return this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH - this.previewWidth(); }
+ @computed get columns() {
+ return Cast(this.props.Document.schemaColumns, listSpec(SchemaHeaderField), []);
+ }
+ set columns(columns: SchemaHeaderField[]) { this.props.Document.schemaColumns = new List<SchemaHeaderField>(columns); }
+ @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); }
+ @computed get tableColumns(): Column<Doc>[] {
+ let possibleKeys = this.documentKeys.filter(key => this.columns.findIndex(existingKey => existingKey.heading.toUpperCase() === key.toUpperCase()) === -1);
+ let columns: Column<Doc>[] = [];
+ let tableIsFocused = this.props.isFocused(this.props.Document);
+ let focusedRow = this._focusedCell.row;
+ let focusedCol = this._focusedCell.col;
+ let isEditable = !this._headerIsEditing;// && this.props.isSelected();
+
+ let cdoc = this.props.dataDoc ? this.props.dataDoc : this.props.Document;
+ let children = DocListCast(cdoc[this.props.fieldKey]);
+
+ if (children.reduce((found, doc) => found || doc.type === "collection", false)) {
+ columns.push(
+ {
+ expander: true,
+ Header: "",
+ width: 30,
+ Expander: (rowInfo) => {
+ if (rowInfo.original.type === "collection") {
+ if (rowInfo.isExpanded) return <div className="collectionSchemaView-expander" onClick={() => this.onCloseCollection(rowInfo.original)}><FontAwesomeIcon icon={"sort-up"} size="sm" /></div>;
+ if (!rowInfo.isExpanded) return <div className="collectionSchemaView-expander" onClick={() => this.onExpandCollection(rowInfo.original)}><FontAwesomeIcon icon={"sort-down"} size="sm" /></div>;
+ } else {
+ return null;
+ }
+ }
}
- return (doc as any)[key][row + ${row}][(doc as any).schemaColumns[col + ${col}]];
- }
- return ${script}`;
- const compiled = CompileScript(script, { params: { this: Doc.name }, typecheck: true, transformer: this.createTransformer(row, col) });
- if (compiled.compiled) {
- doc[field] = new ComputedField(compiled);
- return true;
+ );
}
- return false;
+ let cols = this.columns.map(col => {
+
+ let header = <CollectionSchemaHeader
+ keyValue={col}
+ possibleKeys={possibleKeys}
+ existingKeys={this.columns.map(c => c.heading)}
+ keyType={this.getColumnType(col)}
+ typeConst={col.type !== undefined || columnTypes.get(col.heading) !== undefined}
+ onSelect={this.changeColumns}
+ setIsEditing={this.setHeaderIsEditing}
+ deleteColumn={this.deleteColumn}
+ setColumnType={this.setColumnType}
+ setColumnSort={this.setColumnSort}
+ removeColumnSort={this.removeColumnSort}
+ />;
+
+ return {
+ Header: <MovableColumn columnRenderer={header} columnValue={col} allColumns={this.columns} reorderColumns={this.reorderColumns} ScreenToLocalTransform={this.props.ScreenToLocalTransform} />,
+ accessor: (doc: Doc) => doc ? doc[col.heading] : 0,
+ id: col.heading,
+ Cell: (rowProps: CellInfo) => {
+ let rowIndex = rowProps.index;
+ let columnIndex = this.columns.map(c => c.heading).indexOf(rowProps.column.id!);
+ let isFocused = focusedRow === rowIndex && focusedCol === columnIndex && tableIsFocused;
+
+ let props: CellProps = {
+ row: rowIndex,
+ col: columnIndex,
+ rowProps: rowProps,
+ isFocused: isFocused,
+ changeFocusedCellByIndex: this.changeFocusedCellByIndex,
+ CollectionView: this.props.CollectionView,
+ ContainingCollection: this.props.ContainingCollectionView,
+ Document: this.props.Document,
+ fieldKey: this.props.fieldKey,
+ renderDepth: this.props.renderDepth,
+ addDocTab: this.props.addDocTab,
+ moveDocument: this.props.moveDocument,
+ setIsEditing: this.setCellIsEditing,
+ isEditable: isEditable,
+ setPreviewDoc: this.props.setPreviewDoc
+ };
+
+ let colType = this.getColumnType(col);
+ if (colType === ColumnType.Number) return <CollectionSchemaNumberCell {...props} />;
+ if (colType === ColumnType.String) return <CollectionSchemaStringCell {...props} />;
+ if (colType === ColumnType.Boolean) return <CollectionSchemaCheckboxCell {...props} />;
+ if (colType === ColumnType.Doc) return <CollectionSchemaDocCell {...props} />;
+ return <CollectionSchemaCell {...props} />;
+ },
+ minWidth: 200,
+ };
+ });
+ columns.push(...cols);
+
+ columns.push({
+ Header: <CollectionSchemaAddColumnHeader createColumn={this.createColumn} />,
+ accessor: (doc: Doc) => 0,
+ id: "add",
+ Cell: (rowProps: CellInfo) => <></>,
+ width: 28,
+ resizable: false
+ });
+ return columns;
+ }
+
+ // onHeaderDrag = (columnName: string) => {
+ // let schemaDoc = Cast(this.props.Document.schemaDoc, Doc);
+ // if (schemaDoc instanceof Doc) {
+ // let columnDocs = DocListCast(schemaDoc.data);
+ // if (columnDocs) {
+ // let ddoc = columnDocs.find(doc => doc.title === columnName);
+ // if (ddoc) {
+ // return ddoc;
+ // }
+ // }
+ // }
+ // return this.props.Document;
+ // }
+ constructor(props: SchemaTableProps) {
+ super(props);
+ // convert old schema columns (list of strings) into new schema columns (list of schema header fields)
+ let oldSchemaColumns = Cast(this.props.Document.schemaColumns, listSpec("string"), []);
+ if (oldSchemaColumns && oldSchemaColumns.length) {
+ let newSchemaColumns = oldSchemaColumns.map(i => typeof i === "string" ? new SchemaHeaderField(i) : i);
+ this.props.Document.schemaColumns = new List<SchemaHeaderField>(newSchemaColumns);
+ }
}
- renderCell = (rowProps: CellInfo) => {
- let props: FieldViewProps = {
- Document: rowProps.original,
- DataDoc: rowProps.original,
- fieldKey: rowProps.column.id as string,
- fieldExt: "",
- ContainingCollectionView: this.props.CollectionView,
- isSelected: returnFalse,
- select: emptyFunction,
- renderDepth: this.props.renderDepth + 1,
- selectOnLoad: false,
- ScreenToLocalTransform: Transform.Identity,
- focus: emptyFunction,
- active: returnFalse,
- whenActiveChanged: emptyFunction,
- PanelHeight: returnZero,
- PanelWidth: returnZero,
- addDocTab: this.props.addDocTab,
- };
- let fieldContentView = <FieldView {...props} />;
- let reference = React.createRef<HTMLDivElement>();
- let onItemDown = (e: React.PointerEvent) => {
- (!this.props.CollectionView.props.isSelected() ? undefined :
- SetupDrag(reference, () => props.Document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e));
- };
- let applyToDoc = (doc: Doc, row: number, column: number, run: (args?: { [name: string]: any }) => any) => {
- const res = run({ this: doc, $r: row, $c: column, $: (r: number = 0, c: number = 0) => this.getField(r + row, c + column) });
- if (!res.success) return false;
- doc[props.fieldKey] = res.result;
+ componentDidMount() {
+ document.addEventListener("keydown", this.onKeyDown);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener("keydown", this.onKeyDown);
+ }
+
+ tableAddDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => {
+ return Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before);
+ }
+
+ tableRemoveDoc = (document: Doc): boolean => {
+ let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document;
+ let children = Cast(doc[this.props.fieldKey], listSpec(Doc), []);
+ if (children.indexOf(document) !== -1) {
+ children.splice(children.indexOf(document), 1);
return true;
- };
- const colIndex = this.columns.indexOf(rowProps.column.id!);
- return (
- <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} key={props.Document[Id]} ref={reference}>
- <EditableView
- display={"inline"}
- contents={fieldContentView}
- height={Number(MAX_ROW_HEIGHT)}
- GetValue={() => {
- let field = props.Document[props.fieldKey];
- if (Field.IsField(field)) {
- return Field.toScriptString(field);
- }
- return "";
- }}
- SetValue={(value: string) => {
- if (value.startsWith(":=")) {
- return this.setComputed(value.substring(2), props.Document, rowProps.column.id!, rowProps.index, colIndex);
- }
- let script = CompileScript(value, { addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
- if (!script.compiled) {
- return false;
- }
- return applyToDoc(props.Document, rowProps.index, colIndex, script.run);
- }}
- OnFillDown={async (value: string) => {
- let script = CompileScript(value, { addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
- if (!script.compiled) {
- return;
- }
- const run = script.run;
- const val = await DocListCastAsync(this.props.Document[this.props.fieldKey]);
- val && val.forEach((doc, i) => applyToDoc(doc, i, colIndex, run));
- }}>
- </EditableView>
- </div >
- );
+ }
+ return false;
}
private getTrProps: ComponentPropsGetterR = (state, rowInfo) => {
@@ -244,70 +437,70 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
return {};
}
return {
- onClick: action((e: React.MouseEvent, handleOriginal: Function) => {
- that.props.select(e.ctrlKey);
- that._selectedIndex = rowInfo.index;
+ ScreenToLocalTransform: this.props.ScreenToLocalTransform,
+ addDoc: this.tableAddDoc,
+ removeDoc: this.tableRemoveDoc,
+ // removeDoc: this.props.deleteDocument,
+ rowInfo,
+ rowFocused: !this._headerIsEditing && rowInfo.index === this._focusedCell.row && this.props.isFocused(this.props.Document),
+ textWrapRow: this.textWrapRow,
+ rowWrapped: this._textWrappedRows.findIndex(id => rowInfo.original[Id] === id) > -1
+ };
+ }
- if (handleOriginal) {
- handleOriginal();
- }
- }),
+ private getTdProps: ComponentPropsGetterR = (state, rowInfo, column, instance) => {
+ if (!rowInfo) return {};
+ if (!column) return {};
+
+ let row = rowInfo.index;
+ //@ts-ignore
+ let col = this.columns.map(c => c.heading).indexOf(column!.id);
+ // let col = column ? this.columns.indexOf(column!) : -1;
+ let isFocused = this._focusedCell.row === row && this._focusedCell.col === col && this.props.isFocused(this.props.Document);
+ // let column = this.columns.indexOf(column.id!);
+ return {
style: {
- background: rowInfo.index === this._selectedIndex ? "lightGray" : "white",
- //color: rowInfo.index === this._selectedIndex ? "white" : "black"
+ border: !this._headerIsEditing && isFocused ? "2px solid rgb(255, 160, 160)" : "1px solid #f1efeb"
}
};
}
- private createTarget = (ele: HTMLDivElement) => {
- this._mainCont = ele;
- super.CreateDropTarget(ele);
- }
+ // private createTarget = (ele: HTMLDivElement) => {
+ // this._mainCont = ele;
+ // this.props.CreateDropTarget(ele);
+ // }
+
+ // detectClick = (e: PointerEvent): void => {
+ // if (this._node && this._node.contains(e.target as Node)) {
+ // } else {
+ // this._isOpen = false;
+ // this.props.setIsEditing(false);
+ // }
+ // }
@action
- toggleKey = (key: string) => {
- let list = Cast(this.props.Document.schemaColumns, listSpec("string"));
- if (list === undefined) {
- this.props.Document.schemaColumns = list = new List<string>([key]);
- } else {
- const index = list.indexOf(key);
- if (index === -1) {
- list.push(key);
- } else {
- list.splice(index, 1);
- }
- }
+ onExpandCollection = (collection: Doc): void => {
+ this._openCollections.push(collection[Id]);
}
- //toggles preview side-panel of schema
@action
- toggleExpander = () => {
- this.props.Document.schemaPreviewWidth = this.previewWidth() === 0 ? Math.min(this.tableWidth / 3, 200) : 0;
+ onCloseCollection = (collection: Doc): void => {
+ let index = this._openCollections.findIndex(col => col === collection[Id]);
+ if (index > -1) this._openCollections.splice(index, 1);
}
- onDividerDown = (e: React.PointerEvent) => {
- this._startPreviewWidth = this.previewWidth();
- e.stopPropagation();
- e.preventDefault();
- document.addEventListener("pointermove", this.onDividerMove);
- document.addEventListener('pointerup', this.onDividerUp);
- }
@action
- onDividerMove = (e: PointerEvent): void => {
- let nativeWidth = this._mainCont!.getBoundingClientRect();
- this.props.Document.schemaPreviewWidth = Math.min(nativeWidth.right - nativeWidth.left - 40,
- this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0]);
+ setCellIsEditing = (isEditing: boolean): void => {
+ this._cellIsEditing = isEditing;
}
+
@action
- onDividerUp = (e: PointerEvent): void => {
- document.removeEventListener("pointermove", this.onDividerMove);
- document.removeEventListener('pointerup', this.onDividerUp);
- if (this._startPreviewWidth === this.previewWidth()) {
- this.toggleExpander();
- }
+ setHeaderIsEditing = (isEditing: boolean): void => {
+ this._headerIsEditing = isEditing;
}
onPointerDown = (e: React.PointerEvent): void => {
+ this.props.setFocused(this.props.Document);
if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) {
if (this.props.isSelected()) e.stopPropagation();
}
@@ -319,56 +512,175 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
}
}
- onContextMenu = (e: React.MouseEvent): void => {
- if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
- ContextMenu.Instance.addItem({ description: "Make DB", event: this.makeDB, icon: "table" });
+ onKeyDown = (e: KeyboardEvent): void => {
+ if (!this._cellIsEditing && !this._headerIsEditing && this.props.isFocused(this.props.Document)) {// && this.props.isSelected()) {
+ let direction = e.key === "Tab" ? "tab" : e.which === 39 ? "right" : e.which === 37 ? "left" : e.which === 38 ? "up" : e.which === 40 ? "down" : "";
+ this.changeFocusedCellByDirection(direction);
+
+ let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document;
+ let children = Cast(doc[this.props.fieldKey], listSpec(Doc), []);
+ const pdoc = FieldValue(children[this._focusedCell.row]);
+ pdoc && this.props.setPreviewDoc(pdoc);
}
}
@action
- makeDB = async () => {
- let csv: string = this.columns.reduce((val, col) => val + col + ",", "");
- csv = csv.substr(0, csv.length - 1) + "\n";
- let self = this;
- DocListCast(this.props.Document.data).map(doc => {
- csv += self.columns.reduce((val, col) => val + (doc[col] ? doc[col]!.toString() : "0") + ",", "");
- csv = csv.substr(0, csv.length - 1) + "\n";
- });
- csv.substring(0, csv.length - 1);
- let dbName = StrCast(this.props.Document.title);
- let res = await Gateway.Instance.PostSchema(csv, dbName);
- if (self.props.CollectionView.props.addDocument) {
- let schemaDoc = await Docs.Create.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName }, { dbDoc: self.props.Document });
- if (schemaDoc) {
- //self.props.CollectionView.props.addDocument(schemaDoc, false);
- self.props.Document.schemaDoc = schemaDoc;
+ changeFocusedCellByDirection = (direction: string): void => {
+ let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document;
+ let children = Cast(doc[this.props.fieldKey], listSpec(Doc), []);
+ switch (direction) {
+ case "tab":
+ if (this._focusedCell.col + 1 === this.columns.length && this._focusedCell.row + 1 === children.length) {
+ this._focusedCell = { row: 0, col: 0 };
+ } else if (this._focusedCell.col + 1 === this.columns.length) {
+ this._focusedCell = { row: this._focusedCell.row + 1, col: 0 };
+ } else {
+ this._focusedCell = { row: this._focusedCell.row, col: this._focusedCell.col + 1 };
+ }
+ break;
+ case "right":
+ this._focusedCell = { row: this._focusedCell.row, col: this._focusedCell.col + 1 === this.columns.length ? this._focusedCell.col : this._focusedCell.col + 1 };
+ break;
+ case "left":
+ this._focusedCell = { row: this._focusedCell.row, col: this._focusedCell.col === 0 ? this._focusedCell.col : this._focusedCell.col - 1 };
+ break;
+ case "up":
+ this._focusedCell = { row: this._focusedCell.row === 0 ? this._focusedCell.row : this._focusedCell.row - 1, col: this._focusedCell.col };
+ break;
+ case "down":
+ this._focusedCell = { row: this._focusedCell.row + 1 === children.length ? this._focusedCell.row : this._focusedCell.row + 1, col: this._focusedCell.col };
+ break;
+ }
+ // const pdoc = FieldValue(children[this._focusedCell.row]);
+ // pdoc && this.props.setPreviewDoc(pdoc);
+ }
+
+ @action
+ changeFocusedCellByIndex = (row: number, col: number): void => {
+ let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document;
+ let children = Cast(doc[this.props.fieldKey], listSpec(Doc), []);
+
+ this._focusedCell = { row: row, col: col };
+ this.props.setFocused(this.props.Document);
+
+ // const fdoc = FieldValue(children[this._focusedCell.row]);
+ // fdoc && this.props.setPreviewDoc(fdoc);
+ }
+
+ createRow = () => {
+ let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document;
+ let children = Cast(doc[this.props.fieldKey], listSpec(Doc), []);
+
+ let newDoc = Docs.Create.TextDocument({ width: 100, height: 30 });
+ let proto = Doc.GetProto(newDoc);
+ proto.title = "";
+ children.push(newDoc);
+ }
+
+ @action
+ createColumn = () => {
+ let index = 0;
+ let found = this.columns.findIndex(col => col.heading.toUpperCase() === "New field".toUpperCase()) > -1;
+ if (!found) {
+ this.columns.push(new SchemaHeaderField("New field"));
+ return;
+ }
+ while (found) {
+ index++;
+ found = this.columns.findIndex(col => col.heading.toUpperCase() === ("New field (" + index + ")").toUpperCase()) > -1;
+ }
+ this.columns.push(new SchemaHeaderField("New field (" + index + ")"));
+ }
+
+ @action
+ deleteColumn = (key: string) => {
+ let list = Cast(this.props.Document.schemaColumns, listSpec(SchemaHeaderField));
+ if (list === undefined) {
+ this.props.Document.schemaColumns = list = new List<SchemaHeaderField>([]);
+ } else {
+ const index = list.map(c => c.heading).indexOf(key);
+ if (index > -1) {
+ list.splice(index, 1);
}
}
}
@action
- addColumn = () => {
- this.columns.push(this._newKeyName);
- this._newKeyName = "";
+ changeColumns = (oldKey: string, newKey: string, addNew: boolean) => {
+ let list = Cast(this.props.Document.schemaColumns, listSpec(SchemaHeaderField));
+ if (list === undefined) {
+ this.props.Document.schemaColumns = list = new List<SchemaHeaderField>([new SchemaHeaderField(newKey)]);
+ } else {
+ if (addNew) {
+ this.columns.push(new SchemaHeaderField(newKey));
+ } else {
+ const index = list.map(c => c.heading).indexOf(oldKey);
+ if (index > -1) {
+ list[index] = new SchemaHeaderField(newKey);
+ }
+ }
+ }
+ }
+
+ getColumnType = (column: SchemaHeaderField): ColumnType => {
+ // added functionality to convert old column type stuff to new column type stuff -syip
+ if (column.type && column.type !== 0) {
+ return column.type;
+ }
+ if (columnTypes.get(column.heading)) {
+ column.type = columnTypes.get(column.heading)!;
+ return columnTypes.get(column.heading)!;
+ }
+ const typesDoc = FieldValue(Cast(this.props.Document.schemaColumnTypes, Doc));
+ if (!typesDoc) {
+ column.type = ColumnType.Any;
+ return ColumnType.Any;
+ }
+ column.type = NumCast(typesDoc[column.heading]);
+ return NumCast(typesDoc[column.heading]);
+ }
+
+ setColumnType = (key: string, type: ColumnType): void => {
+ if (columnTypes.get(key)) return;
+ const typesDoc = FieldValue(Cast(this.props.Document.schemaColumnTypes, Doc));
+ if (!typesDoc) {
+ let newTypesDoc = new Doc();
+ newTypesDoc[key] = type;
+ this.props.Document.schemaColumnTypes = newTypesDoc;
+ return;
+ } else {
+ typesDoc[key] = type;
+ }
}
@action
- newKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- this._newKeyName = e.currentTarget.value;
+ setColumns = (columns: SchemaHeaderField[]) => {
+ this.columns = columns;
}
- @computed
- get previewDocument(): Doc | undefined {
- const selected = this.childDocs.length > this._selectedIndex ? this.childDocs[this._selectedIndex] : undefined;
- let pdc = selected ? (this.previewScript && this.previewScript !== "this" ? FieldValue(Cast(selected[this.previewScript], Doc)) : selected) : undefined;
- return pdc;
+ reorderColumns = (toMove: SchemaHeaderField, relativeTo: SchemaHeaderField, before: boolean, columnsValues: SchemaHeaderField[]) => {
+ let columns = [...columnsValues];
+ let oldIndex = columns.indexOf(toMove);
+ let relIndex = columns.indexOf(relativeTo);
+ let newIndex = (oldIndex > relIndex && !before) ? relIndex + 1 : (oldIndex < relIndex && before) ? relIndex - 1 : relIndex;
+
+ if (oldIndex === newIndex) return;
+
+ columns.splice(newIndex, 0, columns.splice(oldIndex, 1)[0]);
+ this.setColumns(columns);
}
- getPreviewTransform = (): Transform => this.props.ScreenToLocalTransform().translate(
- - this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth, - this.borderWidth)
+ @action
+ setColumnSort = (column: string, descending: boolean) => {
+ this._sortedColumns.set(column, { id: column, desc: descending });
+ }
+ @action
+ removeColumnSort = (column: string) => {
+ this._sortedColumns.delete(column);
+ }
- get documentKeysCheckList() {
+ get documentKeys() {
const docs = DocListCast(this.props.Document[this.props.fieldKey]);
let keys: { [key: string]: boolean } = {};
// bcz: ugh. this is untracked since otherwise a large collection of documents will blast the server for all their fields.
@@ -379,99 +691,101 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
//TODO Types
untracked(() => docs.map(doc => Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => keys[key] = false))));
- this.columns.forEach(key => keys[key] = true);
- return Array.from(Object.keys(keys)).map(item =>
- (<KeyToggle checked={keys[item]} key={item} keyName={item} toggle={this.toggleKey} />));
- }
-
- get tableOptionsPanel() {
- return !this.props.active() ? (null) :
- (<Flyout
- anchorPoint={anchorPoints.RIGHT_TOP}
- content={<div>
- <div id="schema-options-header"><h5><b>Options</b></h5></div>
- <div id="options-flyout-div">
- <h6 className="schema-options-subHeader">Preview Window</h6>
- <div id="preview-schema-checkbox-div"><input type="checkbox" key={"Show Preview"} checked={this.previewWidth() !== 0} onChange={this.toggleExpander} /> Show Preview </div>
- <h6 className="schema-options-subHeader" >Displayed Columns</h6>
- <ul id="schema-col-checklist" >
- {this.documentKeysCheckList}
- </ul>
- <input value={this._newKeyName} onChange={this.newKeyChange} />
- <button onClick={this.addColumn}><FontAwesomeIcon style={{ color: "white" }} icon="plus" size="lg" /></button>
- </div>
- </div>
- }>
- <button id="schemaOptionsMenuBtn" ><FontAwesomeIcon style={{ color: "white" }} icon="cog" size="sm" /></button>
- </Flyout>);
+ this.columns.forEach(key => keys[key.heading] = true);
+ return Array.from(Object.keys(keys));
+ }
+
+ @action
+ textWrapRow = (doc: Doc): void => {
+ let index = this._textWrappedRows.findIndex(id => doc[Id] === id);
+ if (index > -1) {
+ this._textWrappedRows.splice(index, 1);
+ } else {
+ this._textWrappedRows.push(doc[Id]);
+ }
+
}
@computed
get reactTable() {
- let previewWidth = this.previewWidth() + 2 * this.borderWidth + this.DIVIDER_WIDTH + 1;
- return <ReactTable style={{ position: "relative", float: "left", width: `calc(100% - ${previewWidth}px` }} data={this.childDocs} page={0} pageSize={this.childDocs.length} showPagination={false}
+
+ let cdoc = this.props.dataDoc ? this.props.dataDoc : this.props.Document;
+ let children = DocListCast(cdoc[this.props.fieldKey]);
+
+ let previewWidth = this.previewWidth(); // + 2 * this.borderWidth + this.DIVIDER_WIDTH + 1;
+ let hasCollectionChild = children.reduce((found, doc) => found || doc.type === "collection", false);
+ let expandedRowsList = this._openCollections.map(col => children.findIndex(doc => doc[Id] === col).toString());
+ let expanded = {};
+ //@ts-ignore
+ expandedRowsList.forEach(row => expanded[row] = true);
+ console.log(...[...this._textWrappedRows]); // TODO: get component to rerender on text wrap change without needign to console.log :((((
+
+ return <ReactTable
+ style={{ position: "relative", float: "left", width: `calc(100% - ${previewWidth}px` }}
+ data={children}
+ page={0}
+ pageSize={children.length}
+ showPagination={false}
columns={this.tableColumns}
- column={{ ...ReactTableDefaults.column, Cell: this.renderCell, }}
getTrProps={this.getTrProps}
+ getTdProps={this.getTdProps}
+ sortable={false}
+ TrComponent={MovableRow}
+ sorted={Array.from(this._sortedColumns.values())}
+ expanded={expanded}
+ SubComponent={hasCollectionChild ?
+ row => {
+ if (row.original.type === "collection") {
+ // let childDocs = DocListCast(row.original[this.props.fieldKey]);
+ return <div className="sub"><SchemaTable {...this.props} Document={row.original} /></div>;
+ }
+ }
+ : undefined}
+
/>;
}
- @computed
- get dividerDragger() {
- return this.previewWidth() === 0 ? (null) :
- <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} />;
+ onContextMenu = (e: React.MouseEvent): void => {
+ if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
+ ContextMenu.Instance.addItem({ description: "Make DB", event: this.makeDB, icon: "table" });
+ }
}
-
- @computed
- get previewPanel() {
- // let layoutDoc = this.previewDocument;
- // let resolvedDataDoc = (layoutDoc !== this.props.DataDoc) ? this.props.DataDoc : undefined;
- // if (layoutDoc && !(Cast(layoutDoc.layout, Doc) instanceof Doc) &&
- // resolvedDataDoc && resolvedDataDoc !== layoutDoc) {
- // // ... so change the layout to be an expanded view of the template layout. This allows the view override the template's properties and be referenceable as its own document.
- // layoutDoc = Doc.expandTemplateLayout(layoutDoc, resolvedDataDoc);
- // }
-
- let layoutDoc = this.previewDocument ? Doc.expandTemplateLayout(this.previewDocument, this.props.DataDoc) : undefined;
- return <div ref={this.createTarget}>
- <CollectionSchemaPreview
- Document={layoutDoc}
- DataDocument={this.previewDocument !== this.props.DataDoc ? this.props.DataDoc : undefined}
- childDocs={this.childDocs}
- renderDepth={this.props.renderDepth}
- width={this.previewWidth}
- height={this.previewHeight}
- getTransform={this.getPreviewTransform}
- CollectionView={this.props.CollectionView}
- moveDocument={this.props.moveDocument}
- addDocument={this.props.addDocument}
- removeDocument={this.props.removeDocument}
- active={this.props.active}
- whenActiveChanged={this.props.whenActiveChanged}
- addDocTab={this.props.addDocTab}
- setPreviewScript={this.setPreviewScript}
- previewScript={this.previewScript}
- />
- </div>;
- }
@action
- setPreviewScript = (script: string) => {
- this.previewScript = script;
+ makeDB = async () => {
+ let csv: string = this.columns.reduce((val, col) => val + col + ",", "");
+ csv = csv.substr(0, csv.length - 1) + "\n";
+ let self = this;
+ DocListCast(this.props.Document.data).map(doc => {
+ csv += self.columns.reduce((val, col) => val + (doc[col.heading] ? doc[col.heading]!.toString() : "0") + ",", "");
+ csv = csv.substr(0, csv.length - 1) + "\n";
+ });
+ csv.substring(0, csv.length - 1);
+ let dbName = StrCast(this.props.Document.title);
+ let res = await Gateway.Instance.PostSchema(csv, dbName);
+ if (self.props.CollectionView.props.addDocument) {
+ let schemaDoc = await Docs.Create.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName }, { dbDoc: self.props.Document });
+ if (schemaDoc) {
+ //self.props.CollectionView.props.addDocument(schemaDoc, false);
+ self.props.Document.schemaDoc = schemaDoc;
+ }
+ }
}
render() {
+ // if (SelectionManager.SelectedDocuments().length > 0) console.log(StrCast(SelectionManager.SelectedDocuments()[0].Document.title));
+ // if (DocumentManager.Instance.getDocumentView(this.props.Document)) console.log(StrCast(this.props.Document.title), SelectionManager.IsSelected(DocumentManager.Instance.getDocumentView(this.props.Document)!))
return (
- <div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} onWheel={this.onWheel}
- onDrop={(e: React.DragEvent) => this.onDrop(e, {})} onContextMenu={this.onContextMenu} ref={this.createTarget}>
+ <div className="collectionSchemaView-table" onPointerDown={this.onPointerDown} onWheel={this.onWheel}
+ onDrop={(e: React.DragEvent) => this.props.onDrop(e, {})} onContextMenu={this.onContextMenu} >
{this.reactTable}
- {this.dividerDragger}
- {!this.previewWidth() ? (null) : this.previewPanel}
- {this.tableOptionsPanel}
+ <button onClick={() => this.createRow()}>new row</button>
</div>
);
}
}
+
+
interface CollectionSchemaPreviewProps {
Document?: Doc;
DataDocument?: Doc;
@@ -550,6 +864,8 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
}
return undefined;
}
+
+
render() {
let input = this.props.previewScript === undefined ? (null) :
<div ref={this.createTarget}><input className="collectionSchemaView-input" value={this.props.previewScript} onChange={this.onPreviewScriptChange}
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 7ebf5f77c..9dbe4ccb8 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -1,9 +1,14 @@
@import "../globalCssVariables";
+
.collectionStackingView {
height: 100%;
width: 100%;
position: absolute;
+ display: flex;
overflow-y: auto;
+ flex-wrap: wrap;
+ transition: top .5s;
+
.collectionStackingView-docView-container {
width: 45%;
margin: 5% 2.5%;
@@ -18,21 +23,23 @@
align-items: center;
}
- .collectionStackingView-masonrySingle, .collectionStackingView-masonryGrid {
- width:100%;
- height:100%;
+ .collectionStackingView-masonrySingle,
+ .collectionStackingView-masonryGrid {
+ width: 100%;
+ height: 100%;
position: absolute;
- display:grid;
+ display: grid;
top: 0;
left: 0;
width: 100%;
position: absolute;
}
+
.collectionStackingView-masonrySingle {
- width:100%;
- height:100%;
+ width: 100%;
+ height: 100%;
position: absolute;
- display:flex;
+ display: flex;
flex-direction: column;
top: 0;
left: 0;
@@ -52,34 +59,126 @@
}
.collectionStackingView-columnDragger {
- width: 15;
- height: 15;
+ width: 15;
+ height: 15;
position: absolute;
margin-left: -5;
}
- .collectionStackingView-columnDoc{
+ .collectionStackingView-columnDoc {
display: inline-block;
}
- .collectionStackingView-columnDoc,
- .collectionStackingView-masonryDoc {
- margin-left: auto;
- margin-right: auto;
- }
-
.collectionStackingView-masonryDoc {
transform-origin: top left;
grid-column-end: span 1;
height: 100%;
}
- .collectionStackingView-sectionHeader {
- width: 90%;
- background: gray;
+
+ .collectionStackingView-sectionHeader {
text-align: center;
- margin-left: 5%;
- margin-right: 5%;
- color: white;
+ margin-left: 5px;
+ margin-right: 5px;
margin-top: 10px;
+ overflow: hidden;
+
+ .editableView-input {
+ color: black;
+ }
+
+ .editableView-input:hover,
+ .editableView-container-editing:hover,
+ .editableView-container-editing-oneLine:hover {
+ cursor: text
+ }
+
+ .collectionStackingView-sectionHeader-subCont {
+ outline: none;
+ border: 0px;
+ color: $light-color;
+ letter-spacing: 2px;
+ font-size: 75%;
+ transition: transform 0.2s;
+ position: relative;
+
+ .editableView-container-editing-oneLine,
+ .editableView-container-editing {
+ color: grey;
+ padding: 10px;
+ }
+
+ .editableView-input:hover,
+ .editableView-container-editing:hover,
+ .editableView-container-editing-oneLine:hover {
+ cursor: text
+ }
+
+ .editableView-input {
+ padding: 12px 10px 11px 10px;
+ border: 0px;
+ color: grey;
+ text-align: center;
+ letter-spacing: 2px;
+ outline-color: black;
+ }
+ }
+
+ .collectionStackingView-sectionDelete {
+ position: absolute;
+ right: 0;
+ top: 0;
+ height: 100%;
+ }
+ }
+
+ .collectionStackingView-addDocumentButton,
+ .collectionStackingView-addGroupButton {
+ display: inline-block;
+ margin: 0 5px;
+ overflow: hidden;
+ width: 90%;
+ color: lightgrey;
+ overflow: ellipses;
+
+ .editableView-container-editing-oneLine,
+ .editableView-container-editing {
+ color: grey;
+ padding: 10px;
+ }
+
+ .editableView-input:hover,
+ .editableView-container-editing:hover,
+ .editableView-container-editing-oneLine:hover {
+ cursor: text
+ }
+
+ .editableView-input {
+ outline-color: black;
+ letter-spacing: 2px;
+ color: grey;
+ border: 0px;
+ padding: 12px 10px 11px 10px;
+ }
+ }
+
+ .collectionStackingView-addDocumentButton {
+ font-size: 75%;
+ letter-spacing: 2px;
+
+ .editableView-input {
+ outline-color: black;
+ letter-spacing: 2px;
+ color: grey;
+ border: 0px;
+ padding: 12px 10px 11px 10px;
+ }
+ }
+
+ .collectionStackingView-addGroupButton {
+ background: rgb(238, 238, 238);
+ font-size: 75%;
+ text-align: center;
+ letter-spacing: 2px;
+ height: fit-content;
}
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index dab03f052..b96b1f8c8 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -2,32 +2,41 @@ import React = require("react");
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { action, computed, IReactionDisposer, reaction, untracked, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
-import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc";
+import { Doc, HeightSym, WidthSym, DocListCast } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
import { BoolCast, NumCast, Cast, StrCast } from "../../../new_fields/Types";
import { emptyFunction, Utils, returnTrue } from "../../../Utils";
import { CollectionSchemaPreview } from "./CollectionSchemaView";
import "./CollectionStackingView.scss";
-import { CollectionSubView } from "./CollectionSubView";
+import { CollectionSubView, SubCollectionViewProps } from "./CollectionSubView";
import { undoBatch } from "../../util/UndoManager";
import { DragManager } from "../../util/DragManager";
import { DocumentType } from "../../documents/Documents";
import { Transform } from "../../util/Transform";
import { CursorProperty } from "csstype";
+import { CollectionStackingViewFieldColumn } from "./CollectionStackingViewFieldColumn";
+import { listSpec } from "../../../new_fields/Schema";
+import { SchemaHeaderField, RandomPastel } from "../../../new_fields/SchemaHeaderField";
+import { List } from "../../../new_fields/List";
+import { EditableView } from "../EditableView";
+import { CollectionViewProps } from "./CollectionBaseView";
@observer
export class CollectionStackingView extends CollectionSubView(doc => doc) {
_masonryGridRef: HTMLDivElement | null = null;
_draggerRef = React.createRef<HTMLDivElement>();
_heightDisposer?: IReactionDisposer;
+ _sectionFilterDisposer?: IReactionDisposer;
_docXfs: any[] = [];
_columnStart: number = 0;
@observable private cursor: CursorProperty = "grab";
- @computed get xMargin() { return NumCast(this.layoutDoc.xMargin, 2 * this.gridGap); }
- @computed get yMargin() { return NumCast(this.layoutDoc.yMargin, 2 * this.gridGap); }
- @computed get gridGap() { return NumCast(this.layoutDoc.gridGap, 10); }
- @computed get singleColumn() { return BoolCast(this.layoutDoc.singleColumn, true); }
- @computed get columnWidth() { return this.singleColumn ? (this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin) : Math.min(this.props.PanelWidth() - 2 * this.xMargin, NumCast(this.layoutDoc.columnWidth, 250)); }
+ get sectionHeaders() { return Cast(this.props.Document.sectionHeaders, listSpec(SchemaHeaderField)); }
+ @computed get chromeCollapsed() { return this.props.chromeCollapsed; }
+ @computed get xMargin() { return NumCast(this.props.Document.xMargin, 2 * this.gridGap); }
+ @computed get yMargin() { return NumCast(this.props.Document.yMargin, 2 * this.gridGap); }
+ @computed get gridGap() { return NumCast(this.props.Document.gridGap, 10); }
+ @computed get singleColumn() { return BoolCast(this.props.Document.singleColumn, true); }
+ @computed get columnWidth() { return this.singleColumn ? (this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin) : Math.min(this.props.PanelWidth() - 2 * this.xMargin, NumCast(this.props.Document.columnWidth, 250)); }
@computed get filteredChildren() { return this.childDocs.filter(d => !d.isMinimized); }
get layoutDoc() {
@@ -35,17 +44,41 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
// to determine the render JSX string, otherwise the layout field should directly contain a JSX layout string.
return this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document;
}
- @computed get Sections() {
- let sectionFilter = StrCast(this.layoutDoc.sectionFilter);
- let fields = new Map<object, Doc[]>();
- sectionFilter && this.filteredChildren.map(d => {
- let sectionValue = (d[sectionFilter] ? d[sectionFilter] : "-undefined-") as object;
- if (!fields.has(sectionValue)) fields.set(sectionValue, [d]);
- else fields.get(sectionValue)!.push(d);
- });
+
+ get Sections() {
+ let sectionFilter = StrCast(this.props.Document.sectionFilter);
+ let sectionHeaders = this.sectionHeaders;
+ if (!sectionHeaders) {
+ this.props.Document.sectionHeaders = sectionHeaders = new List();
+ }
+ let fields = new Map<SchemaHeaderField, Doc[]>(sectionHeaders.map(sh => [sh, []]));
+ if (sectionFilter) {
+ this.filteredChildren.map(d => {
+ let sectionValue = (d[sectionFilter] ? d[sectionFilter] : `NO ${sectionFilter.toUpperCase()} VALUE`) as object;
+ // the next five lines ensures that floating point rounding errors don't create more than one section -syip
+ let parsed = parseInt(sectionValue.toString());
+ let castedSectionValue: any = sectionValue;
+ if (!isNaN(parsed)) {
+ castedSectionValue = parsed;
+ }
+
+ // look for if header exists already
+ let existingHeader = sectionHeaders!.find(sh => sh.heading === (castedSectionValue ? castedSectionValue.toString() : `NO ${sectionFilter.toUpperCase()} VALUE`));
+ if (existingHeader) {
+ fields.get(existingHeader)!.push(d);
+ }
+ else {
+ let newSchemaHeader = new SchemaHeaderField(castedSectionValue ? castedSectionValue.toString() : `NO ${sectionFilter.toUpperCase()} VALUE`);
+ fields.set(newSchemaHeader, [d]);
+ sectionHeaders!.push(newSchemaHeader);
+ }
+ });
+ }
return fields;
}
+
componentDidMount() {
+ // is there any reason this needs to exist? -syip
this._heightDisposer = reaction(() => [this.yMargin, this.props.Document[WidthSym](), this.gridGap, this.columnWidth, this.childDocs.map(d => [d.height, d.width, d.zoomBasis, d.nativeHeight, d.nativeWidth, d.isMinimized])],
() => {
if (this.singleColumn && BoolCast(this.props.Document.autoHeight)) {
@@ -57,9 +90,18 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
.height = hgt * (this.props as any).ContentScaling();
}
}, { fireImmediately: true });
+
+ // reset section headers when a new filter is inputted
+ this._sectionFilterDisposer = reaction(
+ () => StrCast(this.props.Document.sectionFilter),
+ () => {
+ this.props.Document.sectionHeaders = new List();
+ }
+ );
}
componentWillUnmount() {
this._heightDisposer && this._heightDisposer();
+ this._sectionFilterDisposer && this._sectionFilterDisposer();
}
@action
@@ -109,32 +151,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
return d[HeightSym]();
}
- getDocTransform(doc: Doc, dref: HTMLDivElement) {
- let { scale, translateX, translateY } = Utils.GetScreenTransform(dref);
- let outerXf = Utils.GetScreenTransform(this._masonryGridRef!);
- let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
- return this.props.ScreenToLocalTransform().translate(offset[0], offset[1]).scale(NumCast(doc.width, 1) / this.columnWidth);
- }
-
- children(docs: Doc[]) {
- this._docXfs.length = 0;
- return docs.map((d, i) => {
- let pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, d)
- let width = () => d.nativeWidth && !BoolCast(d.ignoreAspect) ? Math.min(pair.layout[WidthSym](), this.columnWidth) : this.columnWidth;
- let height = () => this.getDocHeight(pair.layout);
- let dref = React.createRef<HTMLDivElement>();
- let dxf = () => this.getDocTransform(pair.layout, dref.current!);
- this._docXfs.push({ dxf: dxf, width: width, height: height });
- let rowHgtPcnt = height();
- let rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap);
- let style = this.singleColumn ? { width: width(), marginTop: i === 0 ? 0 : this.gridGap, height: `${rowHgtPcnt}` } : { gridRowEnd: `span ${rowSpan}` };
-
- return <div className={`collectionStackingView-${this.singleColumn ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} >
- {this.getDisplayDoc(pair.layout, pair.data, dxf, width)}
- </div>;
- });
- }
-
columnDividerDown = (e: React.PointerEvent) => {
e.stopPropagation();
e.preventDefault();
@@ -214,40 +230,68 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
});
}
- section(heading: string, docList: Doc[]) {
- let cols = this.singleColumn ? 1 : Math.max(1, Math.min(this.filteredChildren.length,
+ section = (heading: SchemaHeaderField | undefined, docList: Doc[]) => {
+ let key = StrCast(this.props.Document.sectionFilter);
+ let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined;
+ let types = docList.length ? docList.map(d => typeof d[key]) : this.childDocs.map(d => typeof d[key]);
+ if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) {
+ type = types[0];
+ }
+ let cols = () => this.singleColumn ? 1 : Math.max(1, Math.min(this.filteredChildren.length,
Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap))));
- let templatecols = "";
- for (let i = 0; i < cols; i++) templatecols += `${this.columnWidth}px `;
- return <div key={heading}>
- {heading ? <div key={`${heading}`} className="collectionStackingView-sectionHeader">{heading}</div> : (null)}
- <div key={`${heading}-stack`} className={`collectionStackingView-masonry${this.singleColumn ? "Single" : "Grid"}`}
- style={{
- padding: this.singleColumn ? `${this.yMargin}px ${this.xMargin}px ${this.yMargin}px ${this.xMargin}px` : `${this.yMargin}px ${this.xMargin}px`,
- margin: "auto",
- width: this.singleColumn ? undefined : `${cols * (this.columnWidth + this.gridGap) + 2 * this.xMargin - this.gridGap}px`,
- height: 'max-content',
- position: "relative",
- gridGap: this.gridGap,
- gridTemplateColumns: this.singleColumn ? undefined : templatecols,
- gridAutoRows: this.singleColumn ? undefined : "0px"
- }}
- >
- {this.children(docList)}
- {this.singleColumn ? (null) : this.columnDragger}
- </div></div>;
+ return <CollectionStackingViewFieldColumn
+ key={heading ? heading.heading : ""}
+ cols={cols}
+ headings={() => Array.from(this.Sections.keys())}
+ heading={heading ? heading.heading : ""}
+ headingObject={heading}
+ docList={docList}
+ parent={this}
+ type={type}
+ createDropTarget={this.createDropTarget} />;
}
+
+ @action
+ addGroup = (value: string) => {
+ if (value) {
+ if (this.sectionHeaders) {
+ this.sectionHeaders.push(new SchemaHeaderField(value));
+ return true;
+ }
+ }
+ return false;
+ }
+
+ sortFunc = (a: [SchemaHeaderField, Doc[]], b: [SchemaHeaderField, Doc[]]): 1 | -1 => {
+ let descending = BoolCast(this.props.Document.stackingHeadersSortDescending);
+ let firstEntry = descending ? b : a;
+ let secondEntry = descending ? a : b;
+ return firstEntry[0].heading > secondEntry[0].heading ? 1 : -1;
+ }
+
render() {
+ let headings = Array.from(this.Sections.keys());
+ let editableViewProps = {
+ GetValue: () => "",
+ SetValue: this.addGroup,
+ contents: "+ ADD A GROUP"
+ };
+ // let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
return (
- <div className="collectionStackingView"
+ <div className="collectionStackingView" style={{ top: this.chromeCollapsed ? 0 : 100 }}
ref={this.createRef} onDrop={this.onDrop.bind(this)} onWheel={(e: React.WheelEvent) => e.stopPropagation()} >
{/* {sectionFilter as boolean ? [
["width > height", this.filteredChildren.filter(f => f[WidthSym]() >= 1 + f[HeightSym]())],
["width = height", this.filteredChildren.filter(f => Math.abs(f[WidthSym]() - f[HeightSym]()) < 1)],
["height > width", this.filteredChildren.filter(f => f[WidthSym]() + 1 <= f[HeightSym]())]]. */}
- {this.layoutDoc.sectionFilter ? Array.from(this.Sections.entries()).
- map(section => this.section(section[0].toString(), section[1])) :
- this.section("", this.filteredChildren)}
+ {this.props.Document.sectionFilter ? Array.from(this.Sections.entries()).sort(this.sortFunc).
+ map(section => this.section(section[0], section[1])) :
+ this.section(undefined, this.filteredChildren)}
+ {this.props.Document.sectionFilter ?
+ <div key={`${this.props.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton"
+ style={{ width: (this.columnWidth / (headings.length + 1)) - 10, marginTop: 10 }}>
+ <EditableView {...editableViewProps} />
+ </div> : null}
</div>
);
}
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
new file mode 100644
index 000000000..1a5d203f9
--- /dev/null
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -0,0 +1,301 @@
+import React = require("react");
+import { observer } from "mobx-react";
+import { number } from "prop-types";
+import { Doc, WidthSym } from "../../../new_fields/Doc";
+import { CollectionStackingView } from "./CollectionStackingView";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { Utils } from "../../../Utils";
+import { NumCast, StrCast, BoolCast } from "../../../new_fields/Types";
+import { EditableView } from "../EditableView";
+import { action, observable, computed } from "mobx";
+import { undoBatch } from "../../util/UndoManager";
+import { DragManager } from "../../util/DragManager";
+import { DocumentManager } from "../../util/DocumentManager";
+import { SelectionManager } from "../../util/SelectionManager";
+import "./CollectionStackingView.scss";
+import { Docs } from "../../documents/Documents";
+import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { ScriptField } from "../../../new_fields/ScriptField";
+import { CompileScript } from "../../util/Scripting";
+import { RichTextField } from "../../../new_fields/RichTextField";
+
+
+interface CSVFieldColumnProps {
+ cols: () => number;
+ headings: () => object[];
+ heading: string;
+ headingObject: SchemaHeaderField | undefined;
+ docList: Doc[];
+ parent: CollectionStackingView;
+ type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined;
+ createDropTarget: (ele: HTMLDivElement) => void;
+}
+
+@observer
+export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldColumnProps> {
+ @observable private _background = "white";
+
+ private _dropRef: HTMLDivElement | null = null;
+ private dropDisposer?: DragManager.DragDropDisposer;
+ private _headerRef: React.RefObject<HTMLDivElement> = React.createRef();
+
+ @observable _heading = this.props.headingObject ? this.props.headingObject.heading : this.props.heading;
+
+ createColumnDropRef = (ele: HTMLDivElement | null) => {
+ this._dropRef = ele;
+ this.dropDisposer && this.dropDisposer();
+ if (ele) {
+ this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.columnDrop.bind(this) } });
+ }
+ }
+
+ @undoBatch
+ @action
+ columnDrop = (e: Event, de: DragManager.DropEvent) => {
+ if (de.data instanceof DragManager.DocumentDragData) {
+ let key = StrCast(this.props.parent.props.Document.sectionFilter);
+ let castedValue = this.getValue(this._heading);
+ if (castedValue) {
+ de.data.droppedDocuments.forEach(d => d[key] = castedValue);
+ }
+ else {
+ de.data.droppedDocuments.forEach(d => d[key] = undefined);
+ }
+ this.props.parent.drop(e, de);
+ e.stopPropagation();
+ }
+ }
+
+ children(docs: Doc[]) {
+ let parent = this.props.parent;
+ this.props.parent._docXfs.length = 0;
+ return docs.map((d, i) => {
+ let layoutDoc = Doc.expandTemplateLayout(d, parent.props.DataDoc);
+ let headings = this.props.headings();
+ let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
+ let pair = Doc.GetLayoutDataDocPair(parent.props.Document, parent.props.DataDoc, parent.props.fieldKey, d)
+ let width = () => (d.nativeWidth && !BoolCast(d.ignoreAspect) ? Math.min(pair.layout[WidthSym](), parent.columnWidth) : parent.columnWidth) / (uniqueHeadings.length + 1);
+ let height = () => parent.getDocHeight(pair.layout);
+ if (parent.singleColumn) {
+ let dxf;
+ let dref = React.createRef<HTMLDivElement>();
+ if (uniqueHeadings.length > 0) {
+ dxf = () => this.getDocTransform(layoutDoc, dref.current!);
+ this.props.parent._docXfs.push({ dxf: dxf, width: width, height: height });
+ }
+ else {
+ //have to add the height of all previous single column sections or the doc decorations will be in the wrong place.
+ dxf = () => this.getSingleDocTransform(layoutDoc, i, width());
+ this.props.parent._docXfs.push({ dxf: dxf, width: width, height: height });
+ }
+ let rowHgtPcnt = height();
+ return <div className="collectionStackingView-columnDoc" key={d[Id]} ref={dref} style={{ width: width(), marginTop: i === 0 ? 0 : parent.gridGap, height: `${rowHgtPcnt}` }} >
+ {this.props.parent.getDisplayDoc(layoutDoc, d, dxf, width)}
+ </div>;
+ } else {
+ let dref = React.createRef<HTMLDivElement>();
+ let dxf = () => this.getDocTransform(layoutDoc, dref.current!);
+ this.props.parent._docXfs.push({ dxf: dxf, width: width, height: height });
+ let rowHgtPcnt = height();
+ let rowSpan = Math.ceil((height() + parent.gridGap) / parent.gridGap);
+ let divStyle = parent.singleColumn ? { width: width(), marginTop: i === 0 ? 0 : parent.gridGap, height: `${rowHgtPcnt}` } : { gridRowEnd: `span ${rowSpan}` };
+ return <div className="collectionStackingView-masonryDoc" key={d[Id]} ref={dref} style={divStyle} >
+ {this.props.parent.getDisplayDoc(layoutDoc, d, dxf, width)}
+ </div>;
+ }
+ });
+ }
+
+ getSingleDocTransform(doc: Doc, ind: number, width: number) {
+ let localY = this.props.parent.filteredChildren.reduce((height, d, i) =>
+ height + (i < ind ? this.props.parent.getDocHeight(Doc.expandTemplateLayout(d, this.props.parent.props.DataDoc)) + this.props.parent.gridGap : 0), this.props.parent.yMargin);
+ let translate = this.props.parent.props.ScreenToLocalTransform().inverse().transformPoint((this.props.parent.props.PanelWidth() - width) / 2, localY);
+ return this.offsetTransform(doc, translate[0], translate[1]);
+ }
+
+ offsetTransform(doc: Doc, translateX: number, translateY: number) {
+ let outerXf = Utils.GetScreenTransform(this.props.parent._masonryGridRef!);
+ let offset = this.props.parent.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
+ return this.props.parent.props.ScreenToLocalTransform().translate(offset[0], offset[1]).scale(NumCast(doc.width, 1) / this.props.parent.columnWidth);
+ }
+
+ getDocTransform(doc: Doc, dref: HTMLDivElement) {
+ let { scale, translateX, translateY } = Utils.GetScreenTransform(dref);
+ let outerXf = Utils.GetScreenTransform(this.props.parent._masonryGridRef!);
+ let offset = this.props.parent.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
+ return this.props.parent.props.ScreenToLocalTransform().translate(offset[0], offset[1]).scale(NumCast(doc.width, 1) / this.props.parent.columnWidth);
+ }
+
+ getValue = (value: string): any => {
+ let parsed = parseInt(value);
+ if (!isNaN(parsed)) {
+ return parsed;
+ }
+ if (value.toLowerCase().indexOf("true") > -1) {
+ return true;
+ }
+ if (value.toLowerCase().indexOf("false") > -1) {
+ return false;
+ }
+ return value;
+ }
+
+ @action
+ headingChanged = (value: string, shiftDown?: boolean) => {
+ let key = StrCast(this.props.parent.props.Document.sectionFilter);
+ let castedValue = this.getValue(value);
+ if (castedValue) {
+ if (this.props.parent.sectionHeaders) {
+ if (this.props.parent.sectionHeaders.map(i => i.heading).indexOf(castedValue.toString()) > -1) {
+ return false;
+ }
+ }
+ this.props.docList.forEach(d => d[key] = castedValue);
+ if (this.props.headingObject) {
+ this.props.headingObject.setHeading(castedValue.toString());
+ this._heading = this.props.headingObject.heading;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @action
+ pointerEntered = () => {
+ if (SelectionManager.GetIsDragging()) {
+ this._background = "#b4b4b4";
+ }
+ }
+
+ @action
+ pointerLeave = () => {
+ this._background = "white";
+ }
+
+ @action
+ addDocument = (value: string, shiftDown?: boolean) => {
+ let key = StrCast(this.props.parent.props.Document.sectionFilter);
+ let newDoc = Docs.Create.TextDocument({ height: 18, width: 200, title: value });
+ newDoc[key] = this.getValue(this.props.heading);
+ return this.props.parent.props.addDocument(newDoc);
+ }
+
+ @action
+ deleteColumn = () => {
+ let key = StrCast(this.props.parent.props.Document.sectionFilter);
+ this.props.docList.forEach(d => d[key] = undefined);
+ if (this.props.parent.sectionHeaders && this.props.headingObject) {
+ let index = this.props.parent.sectionHeaders.indexOf(this.props.headingObject);
+ this.props.parent.sectionHeaders.splice(index, 1);
+ }
+ }
+
+ startDrag = (e: PointerEvent) => {
+ let alias = Doc.MakeAlias(this.props.parent.props.Document);
+ let key = StrCast(this.props.parent.props.Document.sectionFilter);
+ let value = this.getValue(this._heading);
+ value = typeof value === "string" ? `"${value}"` : value;
+ let script = `return doc.${key} === ${value}`;
+ let compiled = CompileScript(script, { params: { doc: Doc.name } });
+ if (compiled.compiled) {
+ let scriptField = new ScriptField(compiled);
+ alias.viewSpecScript = scriptField;
+ let dragData = new DragManager.DocumentDragData([alias], [alias.proto]);
+ DragManager.StartDocumentDrag([this._headerRef.current!], dragData, e.clientX, e.clientY);
+ }
+
+ e.stopPropagation();
+ document.removeEventListener("pointermove", this.startDrag);
+ document.removeEventListener("pointerup", this.pointerUp);
+ }
+
+ pointerUp = (e: PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+
+ document.removeEventListener("pointermove", this.startDrag);
+ document.removeEventListener("pointerup", this.pointerUp);
+ }
+
+ headerDown = (e: React.PointerEvent<HTMLDivElement>) => {
+ e.stopPropagation();
+ e.preventDefault();
+
+ document.removeEventListener("pointermove", this.startDrag);
+ document.addEventListener("pointermove", this.startDrag);
+ document.removeEventListener("pointerup", this.pointerUp);
+ document.addEventListener("pointerup", this.pointerUp);
+ }
+
+ render() {
+ let cols = this.props.cols();
+ let key = StrCast(this.props.parent.props.Document.sectionFilter);
+ let templatecols = "";
+ let headings = this.props.headings();
+ let heading = this._heading;
+ let style = this.props.parent;
+ let singleColumn = style.singleColumn;
+ let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
+ let evContents = heading ? heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`;
+ let headerEditableViewProps = {
+ GetValue: () => evContents,
+ SetValue: this.headingChanged,
+ contents: evContents,
+ oneLine: true
+ }
+ let newEditableViewProps = {
+ GetValue: () => "",
+ SetValue: this.addDocument,
+ contents: "+ NEW"
+ }
+ let headingView = this.props.headingObject ?
+ <div key={heading} className="collectionStackingView-sectionHeader" ref={this._headerRef}
+ style={{ width: (style.columnWidth) / (uniqueHeadings.length + 1) }}>
+ {/* the default bucket (no key value) has a tooltip that describes what it is.
+ Further, it does not have a color and cannot be deleted. */}
+ <div className="collectionStackingView-sectionHeader-subCont" onPointerDown={this.headerDown}
+ title={evContents === `NO ${key.toUpperCase()} VALUE` ?
+ `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ""}
+ style={{
+ width: "100%",
+ background: this.props.headingObject && evContents !== `NO ${key.toUpperCase()} VALUE` ?
+ this.props.headingObject.color : "lightgrey",
+ color: "grey"
+ }}>
+ <EditableView {...headerEditableViewProps} />
+ {evContents === `NO ${key.toUpperCase()} VALUE` ?
+ (null) :
+ <button className="collectionStackingView-sectionDelete" onClick={this.deleteColumn}>
+ <FontAwesomeIcon icon="trash" />
+ </button>}
+ </div>
+ </div> : (null);
+ for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth}px `;
+ return (
+ <div key={heading} style={{ width: `${100 / (uniqueHeadings.length + 1)}%`, background: this._background }}
+ ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}>
+ {headingView}
+ <div key={`${heading}-stack`} className={`collectionStackingView-masonry${singleColumn ? "Single" : "Grid"}`}
+ style={{
+ padding: singleColumn ? `${style.yMargin}px ${0}px ${style.yMargin}px ${0}px` : `${style.yMargin}px ${0}px`,
+ margin: "auto 5px",
+ width: singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`,
+ height: 'max-content',
+ position: "relative",
+ gridGap: style.gridGap,
+ gridTemplateColumns: singleColumn ? undefined : templatecols,
+ gridAutoRows: singleColumn ? undefined : "0px"
+ }}
+ >
+ {this.children(this.props.docList)}
+ {singleColumn ? (null) : this.props.parent.columnDragger}
+ </div>
+ <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton"
+ style={{ width: style.columnWidth / (uniqueHeadings.length + 1) }}>
+ <EditableView {...newEditableViewProps} />
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index cbc9e61eb..a15ed8f94 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -21,6 +21,8 @@ import { CollectionView } from "./CollectionView";
import React = require("react");
import { MainView } from "../MainView";
import { Utils } from "../../../Utils";
+import { ScriptField } from "../../../new_fields/ScriptField";
+import { CompileScript } from "../../util/Scripting";
export interface CollectionViewProps extends FieldViewProps {
addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
@@ -28,6 +30,7 @@ export interface CollectionViewProps extends FieldViewProps {
moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
PanelWidth: () => number;
PanelHeight: () => number;
+ chromeCollapsed: boolean;
}
export interface SubCollectionViewProps extends CollectionViewProps {
@@ -54,7 +57,18 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
let self = this;
//TODO tfs: This might not be what we want?
//This linter error can't be fixed because of how js arguments work, so don't switch this to filter(FieldValue)
- return DocListCast(this.extensionDoc[this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey]);
+ let docs = DocListCast(this.extensionDoc[this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey]);
+ let viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
+ if (viewSpecScript) {
+ let script = viewSpecScript.script;
+ docs = docs.filter(d => {
+ let res = script.run({ doc: d });
+ if (res.success) {
+ return res.result;
+ }
+ });
+ }
+ return docs;
}
get childDocList() {
//TODO tfs: This might not be what we want?
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index 5205f4313..db3652ff6 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -31,6 +31,7 @@
margin-top: 4px;
transform: scale(1.3, 1.3);
}
+
.editableView-container {
font-weight: bold;
}
@@ -43,18 +44,20 @@
display: inline;
}
- .editableView-input, .editableView-container-editing {
+ .editableView-input,
+ .editableView-container-editing {
display: block;
text-overflow: ellipsis;
font-size: 24px;
white-space: nowrap;
}
}
+
.collectionTreeView-keyHeader {
font-style: italic;
font-size: 8pt;
margin-left: 3px;
- display:none;
+ display: none;
background: lightgray;
}
@@ -72,28 +75,31 @@
// width:100%;//width: max-content;
}
+
.treeViewItem-openRight {
display: none;
}
.treeViewItem-border {
- display:inherit;
+ display: inherit;
border-left: dashed 1px #00000042;
}
.treeViewItem-header:hover {
.collectionTreeView-keyHeader {
- display:inherit;
+ display: inherit;
}
+
.treeViewItem-openRight {
display: inline-block;
- height:13px;
- margin-top:2px;
+ height: 13px;
+ margin-top: 2px;
margin-left: 5px;
+
// display: inline;
svg {
- display:block;
- padding:0px;
+ display: block;
+ padding: 0px;
margin: 0px;
}
}
@@ -101,14 +107,17 @@
.treeViewItem-header {
border: transparent 1px solid;
- display:flex;
+ display: flex;
}
+
.treeViewItem-header-above {
border-top: black 1px solid;
}
+
.treeViewItem-header-below {
border-bottom: black 1px solid;
}
+
.treeViewItem-header-inside {
border: black 1px solid;
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 1c7fe4bee..4d31c3ae7 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -497,6 +497,8 @@ export class CollectionTreeView extends CollectionSubView(Document) {
private treedropDisposer?: DragManager.DragDropDisposer;
private _mainEle?: HTMLDivElement;
+ @computed get chromeCollapsed() { return this.props.chromeCollapsed; }
+
protected createTreeDropTarget = (ele: HTMLDivElement) => {
this.treedropDisposer && this.treedropDisposer();
if (this._mainEle = ele) {
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index b4f29755c..81c84852a 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -15,6 +15,10 @@ import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormV
import { CollectionSchemaView } from "./CollectionSchemaView";
import { CollectionStackingView } from './CollectionStackingView';
import { CollectionTreeView } from "./CollectionTreeView";
+import { StrCast, PromiseValue } from '../../../new_fields/Types';
+import { DocumentType } from '../../documents/Documents';
+import { CollectionStackingViewChrome, CollectionViewBaseChrome } from './CollectionViewChromes';
+import { observable, action, runInAction } from 'mobx';
import { faEye } from '@fortawesome/free-regular-svg-icons';
export const COLLECTION_BORDER_WIDTH = 2;
@@ -31,23 +35,53 @@ library.add(faImage, faEye);
@observer
export class CollectionView extends React.Component<FieldViewProps> {
+ @observable private _collapsed = false;
+
public static LayoutString(fieldStr: string = "data", fieldExt: string = "") { return FieldView.LayoutString(CollectionView, fieldStr, fieldExt); }
- private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => {
+ componentDidMount = () => {
+ // chrome status is one of disabled, collapsed, or visible. this determines initial state from document
+ let chromeStatus = this.props.Document.chromeStatus;
+ if (chromeStatus && (chromeStatus === "disabled" || chromeStatus === "collapsed")) {
+ runInAction(() => this._collapsed = true);
+ }
+ }
+
+ private SubViewHelper = (type: CollectionViewType, renderProps: CollectionRenderProps) => {
let props = { ...this.props, ...renderProps };
switch (this.isAnnotationOverlay ? CollectionViewType.Freeform : type) {
- case CollectionViewType.Schema: return (<CollectionSchemaView {...props} CollectionView={this} />);
- case CollectionViewType.Docking: return (<CollectionDockingView {...props} CollectionView={this} />);
- case CollectionViewType.Tree: return (<CollectionTreeView {...props} CollectionView={this} />);
- case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView {...props} CollectionView={this} />); }
- case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView {...props} CollectionView={this} />); }
+ case CollectionViewType.Schema: return (<CollectionSchemaView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />);
+ // currently cant think of a reason for collection docking view to have a chrome. mind may change if we ever have nested docking views -syip
+ case CollectionViewType.Docking: return (<CollectionDockingView chromeCollapsed={true} key="collview" {...props} CollectionView={this} />);
+ case CollectionViewType.Tree: return (<CollectionTreeView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />);
+ case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />); }
+ case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />); }
case CollectionViewType.Freeform:
default:
- return (<CollectionFreeFormView {...props} CollectionView={this} />);
+ return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />);
}
return (null);
}
+ @action
+ private collapse = (value: boolean) => {
+ this._collapsed = value;
+ this.props.Document.chromeStatus = value ? "collapsed" : "visible";
+ }
+
+ private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => {
+ // currently cant think of a reason for collection docking view to have a chrome. mind may change if we ever have nested docking views -syip
+ if (this.isAnnotationOverlay || this.props.Document.chromeStatus === "disabled" || type === CollectionViewType.Docking) {
+ return [(null), this.SubViewHelper(type, renderProps)];
+ }
+ else {
+ return [
+ (<CollectionViewBaseChrome CollectionView={this} type={type} collapse={this.collapse} />),
+ this.SubViewHelper(type, renderProps)
+ ];
+ }
+ }
+
get isAnnotationOverlay() { return this.props.fieldExt ? true : false; }
onContextMenu = (e: React.MouseEvent): void => {
diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss
new file mode 100644
index 000000000..6525f3b07
--- /dev/null
+++ b/src/client/views/collections/CollectionViewChromes.scss
@@ -0,0 +1,168 @@
+@import "../globalCssVariables";
+@import '~js-datepicker/dist/datepicker.min.css';
+
+.collectionViewChrome-cont {
+ position: relative;
+ z-index: 9001;
+ transition: top .5s;
+ background: lightslategray;
+ padding: 10px;
+
+ .collectionViewChrome {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ padding-bottom: 10px;
+ border-bottom: .5px solid lightgrey;
+
+ .collectionViewBaseChrome {
+ display: flex;
+
+ .collectionViewBaseChrome-viewPicker {
+ font-size: 75%;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ background: rgb(238, 238, 238);
+ color: grey;
+ outline-color: black;
+ border: none;
+ padding: 12px 10px 11px 10px;
+ margin-left: 50px;
+ }
+
+ .collectionViewBaseChrome-viewPicker:active {
+ outline-color: black;
+ }
+
+ .collectionViewBaseChrome-collapse {
+ transition: all .5s;
+ position: absolute;
+ width: 40px;
+ }
+
+ .collectionViewBaseChrome-viewSpecs {
+ margin-left: 10px;
+ display: grid;
+
+ .collectionViewBaseChrome-viewSpecsInput {
+ padding: 12px 10px 11px 10px;
+ border: 0px;
+ color: grey;
+ text-align: center;
+ letter-spacing: 2px;
+ outline-color: black;
+ font-size: 75%;
+ background: rgb(238, 238, 238);
+ height: 100%;
+ width: 150px;
+ }
+
+ .collectionViewBaseChrome-viewSpecsMenu {
+ overflow: hidden;
+ transition: height .5s, display .5s;
+ position: absolute;
+ top: 60px;
+ z-index: 100;
+ display: flex;
+ flex-direction: column;
+ background: rgb(238, 238, 238);
+ box-shadow: grey 2px 2px 4px;
+
+ .qs-datepicker {
+ left: unset;
+ right: 0;
+ }
+
+ .collectionViewBaseChrome-viewSpecsMenu-row {
+ display: grid;
+ grid-template-columns: 150px 200px 150px;
+ margin-top: 10px;
+ margin-right: 10px;
+
+ .collectionViewBaseChrome-viewSpecsMenu-rowLeft,
+ .collectionViewBaseChrome-viewSpecsMenu-rowMiddle,
+ .collectionViewBaseChrome-viewSpecsMenu-rowRight {
+ font-size: 75%;
+ letter-spacing: 2px;
+ color: grey;
+ margin-left: 10px;
+ padding: 5px;
+ border: none;
+ outline-color: black;
+ }
+ }
+
+ .collectionViewBaseChrome-viewSpecsMenu-lastRow {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ grid-gap: 10px;
+ margin: 10px;
+ }
+ }
+ }
+ }
+
+
+ .collectionStackingViewChrome-cont {
+ display: flex;
+ justify-content: space-between;
+ }
+
+ .collectionStackingViewChrome-sort {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ .collectionStackingViewChrome-sortIcon {
+ transition: transform .5s;
+ margin-left: 10px;
+ }
+ }
+
+ button:hover {
+ transform: scale(1);
+ }
+
+
+ .collectionStackingViewChrome-sectionFilter-cont {
+ justify-self: right;
+ display: flex;
+ font-size: 75%;
+ letter-spacing: 2px;
+
+ .collectionStackingViewChrome-sectionFilter-label {
+ vertical-align: center;
+ padding: 10px;
+ }
+
+ .collectionStackingViewChrome-sectionFilter {
+ color: white;
+ width: 100px;
+ text-align: center;
+ background: rgb(238, 238, 238);
+
+ .editable-view-input,
+ input,
+ .editableView-container-editing-oneLine,
+ .editableView-container-editing {
+ padding: 12px 10px 11px 10px;
+ border: 0px;
+ color: grey;
+ text-align: center;
+ letter-spacing: 2px;
+ outline-color: black;
+ height: 100%;
+ }
+
+ .react-autosuggest__container {
+ margin: 0;
+ color: grey;
+ padding: 0px;
+ }
+ }
+ }
+
+ .collectionStackingViewChrome-sectionFilter:hover {
+ cursor: text;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
new file mode 100644
index 000000000..de1d01ef0
--- /dev/null
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -0,0 +1,373 @@
+import * as React from "react";
+import { CollectionView } from "./CollectionView";
+import "./CollectionViewChromes.scss";
+import { CollectionViewType } from "./CollectionBaseView";
+import { undoBatch } from "../../util/UndoManager";
+import { action, observable, runInAction, computed, IObservable, IObservableValue } from "mobx";
+import { observer } from "mobx-react";
+import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { DocLike } from "../MetadataEntryMenu";
+import * as Autosuggest from 'react-autosuggest';
+import { EditableView } from "../EditableView";
+import { StrCast, NumCast, BoolCast, Cast } from "../../../new_fields/Types";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { Utils } from "../../../Utils";
+import KeyRestrictionRow from "./KeyRestrictionRow";
+import { CompileScript } from "../../util/Scripting";
+import { ScriptField } from "../../../new_fields/ScriptField";
+import { CollectionSchemaView } from "./CollectionSchemaView";
+import { COLLECTION_BORDER_WIDTH } from "../globalCssVariables.scss";
+const datepicker = require('js-datepicker');
+
+interface CollectionViewChromeProps {
+ CollectionView: CollectionView;
+ type: CollectionViewType;
+ collapse?: (value: boolean) => any;
+}
+
+let stopPropagation = (e: React.SyntheticEvent) => e.stopPropagation();
+
+@observer
+export class CollectionViewBaseChrome extends React.Component<CollectionViewChromeProps> {
+ @observable private _viewSpecsOpen: boolean = false;
+ @observable private _dateWithinValue: string = "";
+ @observable private _dateValue: Date = new Date();
+ @observable private _keyRestrictions: [JSX.Element, string][] = [];
+ @observable private _collapsed: boolean = false;
+ @computed private get filterValue() { return Cast(this.props.CollectionView.props.Document.viewSpecScript, ScriptField); }
+
+ private _picker: any;
+ private _datePickerElGuid = Utils.GenerateGuid();
+
+ componentDidMount = () => {
+ this._picker = datepicker("#" + this._datePickerElGuid, {
+ disabler: (date: Date) => date > new Date(),
+ onSelect: (instance: any, date: Date) => runInAction(() => this._dateValue = date),
+ dateSelected: new Date()
+ });
+
+ runInAction(() => {
+ this._keyRestrictions.push([<KeyRestrictionRow key={Utils.GenerateGuid()} contains={true} script={(value: string) => runInAction(() => this._keyRestrictions[0][1] = value)} />, ""]);
+ this._keyRestrictions.push([<KeyRestrictionRow key={Utils.GenerateGuid()} contains={false} script={(value: string) => runInAction(() => this._keyRestrictions[1][1] = value)} />, ""]);
+
+ // chrome status is one of disabled, collapsed, or visible. this determines initial state from document
+ let chromeStatus = this.props.CollectionView.props.Document.chromeStatus;
+ if (chromeStatus) {
+ if (chromeStatus === "disabled") {
+ throw new Error("how did you get here, if chrome status is 'disabled' on a collection, a chrome shouldn't even be instantiated!");
+ }
+ else if (chromeStatus === "collapsed") {
+ this._collapsed = true;
+ if (this.props.collapse) {
+ this.props.collapse(true);
+ }
+ }
+ }
+ });
+ }
+
+ @undoBatch
+ viewChanged = (e: React.ChangeEvent) => {
+ //@ts-ignore
+ this.props.CollectionView.props.Document.viewType = parseInt(e.target.selectedOptions[0].value);
+ }
+
+ @action
+ openViewSpecs = (e: React.SyntheticEvent) => {
+ this._viewSpecsOpen = true;
+
+ //@ts-ignore
+ if (!e.target.classList[0].startsWith("qs")) {
+ this.closeDatePicker();
+ }
+
+ e.stopPropagation();
+ document.removeEventListener("pointerdown", this.closeViewSpecs);
+ document.addEventListener("pointerdown", this.closeViewSpecs);
+ }
+
+ @action closeViewSpecs = () => { this._viewSpecsOpen = false; document.removeEventListener("pointerdown", this.closeViewSpecs); };
+
+ @action
+ openDatePicker = (e: React.PointerEvent) => {
+ this.openViewSpecs(e);
+ if (this._picker) {
+ this._picker.alwaysShow = true;
+ this._picker.show();
+ // TODO: calendar is offset when zoomed in/out
+ // this._picker.calendar.style.position = "absolute";
+ // let transform = this.props.CollectionView.props.ScreenToLocalTransform();
+ // let x = parseInt(this._picker.calendar.style.left) / transform.Scale;
+ // let y = parseInt(this._picker.calendar.style.top) / transform.Scale;
+ // this._picker.calendar.style.left = x;
+ // this._picker.calendar.style.top = y;
+
+ e.stopPropagation();
+ }
+ }
+
+ @action
+ addKeyRestriction = (e: React.MouseEvent) => {
+ let index = this._keyRestrictions.length;
+ this._keyRestrictions.push([<KeyRestrictionRow key={Utils.GenerateGuid()} contains={true} script={(value: string) => runInAction(() => this._keyRestrictions[index][1] = value)} />, ""]);
+
+ this.openViewSpecs(e);
+ }
+
+ @action
+ applyFilter = (e: React.MouseEvent) => {
+ this.openViewSpecs(e);
+
+ let keyRestrictionScript = `${this._keyRestrictions.map(i => i[1])
+ .reduce((acc: string, value: string, i: number) => value ? `${acc} && ${value}` : acc)}`;
+ let yearOffset = this._dateWithinValue[1] === 'y' ? 1 : 0;
+ let monthOffset = this._dateWithinValue[1] === 'm' ? parseInt(this._dateWithinValue[0]) : 0;
+ let weekOffset = this._dateWithinValue[1] === 'w' ? parseInt(this._dateWithinValue[0]) : 0;
+ let dayOffset = (this._dateWithinValue[1] === 'd' ? parseInt(this._dateWithinValue[0]) : 0) + weekOffset * 7;
+ let lowerBound = new Date(this._dateValue.getFullYear() - yearOffset, this._dateValue.getMonth() - monthOffset, this._dateValue.getDate() - dayOffset);
+ let upperBound = new Date(this._dateValue.getFullYear() + yearOffset, this._dateValue.getMonth() + monthOffset, this._dateValue.getDate() + dayOffset + 1);
+ let dateRestrictionScript = `((doc.creationDate as any).date >= ${lowerBound.valueOf()} && (doc.creationDate as any).date <= ${upperBound.valueOf()})`;
+ let fullScript = `return ${dateRestrictionScript} && ${keyRestrictionScript}`;
+ let compiled = CompileScript(fullScript, { params: { doc: Doc.name } });
+ if (compiled.compiled) {
+ this.props.CollectionView.props.Document.viewSpecScript = new ScriptField(compiled);
+ }
+ }
+
+ @action
+ closeDatePicker = () => {
+ if (this._picker) {
+ this._picker.alwaysShow = false;
+ this._picker.hide();
+ }
+ document.removeEventListener("pointerdown", this.closeDatePicker);
+ }
+
+ @action
+ toggleCollapse = () => {
+ this._collapsed = !this._collapsed;
+ if (this.props.collapse) {
+ this.props.collapse(this._collapsed);
+ }
+ }
+
+ subChrome = () => {
+ switch (this.props.type) {
+ case CollectionViewType.Stacking: return (
+ <CollectionStackingViewChrome
+ key="collchrome"
+ CollectionView={this.props.CollectionView}
+ type={this.props.type} />);
+ case CollectionViewType.Schema: return (
+ <CollectionSchemaViewChrome
+ key="collchrome"
+ CollectionView={this.props.CollectionView}
+ type={this.props.type}
+ />);
+ default:
+ return null;
+ }
+ }
+
+ render() {
+ return (
+ <div className="collectionViewChrome-cont" style={{ top: this._collapsed ? -100 : 0 }}>
+ <div className="collectionViewChrome">
+ <div className="collectionViewBaseChrome">
+ <button className="collectionViewBaseChrome-collapse"
+ style={{ top: this._collapsed ? 90 : 10, transform: `rotate(${this._collapsed ? 180 : 0}deg)` }}
+ title="Collapse collection chrome" onClick={this.toggleCollapse}>
+ <FontAwesomeIcon icon="caret-up" size="2x" />
+ </button>
+ <select
+ className="collectionViewBaseChrome-viewPicker"
+ onPointerDown={stopPropagation}
+ onChange={this.viewChanged}
+ value={NumCast(this.props.CollectionView.props.Document.viewType)}>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="1">Freeform View</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="2">Schema View</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="4">Tree View</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="5">Stacking View</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="6">Masonry View</option>
+ </select>
+ <div className="collectionViewBaseChrome-viewSpecs">
+ <input className="collectionViewBaseChrome-viewSpecsInput"
+ placeholder="FILTER DOCUMENTS"
+ value={this.filterValue ? this.filterValue.script.originalScript : ""}
+ onPointerDown={this.openViewSpecs} />
+ <div className="collectionViewBaseChrome-viewSpecsMenu"
+ onPointerDown={this.openViewSpecs}
+ style={{
+ height: this._viewSpecsOpen ? "fit-content" : "0px",
+ overflow: this._viewSpecsOpen ? "initial" : "hidden"
+ }}>
+ {this._keyRestrictions.map(i => i[0])}
+ <div className="collectionViewBaseChrome-viewSpecsMenu-row">
+ <div className="collectionViewBaseChrome-viewSpecsMenu-rowLeft">
+ CREATED WITHIN:
+ </div>
+ <select className="collectionViewBaseChrome-viewSpecsMenu-rowMiddle"
+ style={{ textTransform: "uppercase", textAlign: "center" }}
+ value={this._dateWithinValue}
+ onChange={(e) => runInAction(() => this._dateWithinValue = e.target.value)}>
+ <option value="1d">1 day of</option>
+ <option value="3d">3 days of</option>
+ <option value="1w">1 week of</option>
+ <option value="2w">2 weeks of</option>
+ <option value="1m">1 month of</option>
+ <option value="2m">2 months of</option>
+ <option value="6m">6 months of</option>
+ <option value="1y">1 year of</option>
+ </select>
+ <input className="collectionViewBaseChrome-viewSpecsMenu-rowRight"
+ id={this._datePickerElGuid}
+ value={this._dateValue.toLocaleDateString()}
+ onPointerDown={this.openDatePicker}
+ placeholder="Value" />
+ </div>
+ <div className="collectionViewBaseChrome-viewSpecsMenu-lastRow">
+ <button className="collectonViewBaseChrome-viewSpecsMenu-lastRowButton" onClick={this.addKeyRestriction}>
+ ADD KEY RESTRICTION
+ </button>
+ <button className="collectonViewBaseChrome-viewSpecsMenu-lastRowButton" onClick={this.applyFilter}>
+ APPLY FILTER
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ {this.subChrome()}
+ </div>
+ </div>
+ );
+ }
+}
+
+@observer
+export class CollectionStackingViewChrome extends React.Component<CollectionViewChromeProps> {
+ @observable private _currentKey: string = "";
+ @observable private suggestions: string[] = [];
+
+ @computed private get descending() { return BoolCast(this.props.CollectionView.props.Document.stackingHeadersSortDescending); }
+ @computed get sectionFilter() { return StrCast(this.props.CollectionView.props.Document.sectionFilter); }
+
+ getKeySuggestions = async (value: string): Promise<string[]> => {
+ value = value.toLowerCase();
+ let docs: Doc | Doc[] | Promise<Doc> | Promise<Doc[]> | (() => DocLike)
+ = () => DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldExt ? this.props.CollectionView.props.fieldExt : this.props.CollectionView.props.fieldKey]);
+ if (typeof docs === "function") {
+ docs = docs();
+ }
+ docs = await docs;
+ if (docs instanceof Doc) {
+ return Object.keys(docs).filter(key => key.toLowerCase().startsWith(value));
+ } else {
+ const keys = new Set<string>();
+ docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key)));
+ return Array.from(keys).filter(key => key.toLowerCase().startsWith(value));
+ }
+ }
+
+ @action
+ onKeyChange = (e: React.ChangeEvent, { newValue }: { newValue: string }) => {
+ this._currentKey = newValue;
+ }
+
+ getSuggestionValue = (suggestion: string) => suggestion;
+
+ renderSuggestion = (suggestion: string) => {
+ return <p>{suggestion}</p>;
+ }
+
+ onSuggestionFetch = async ({ value }: { value: string }) => {
+ const sugg = await this.getKeySuggestions(value);
+ runInAction(() => {
+ this.suggestions = sugg;
+ });
+ }
+
+ @action
+ onSuggestionClear = () => {
+ this.suggestions = [];
+ }
+
+ setValue = (value: string) => {
+ this.props.CollectionView.props.Document.sectionFilter = value;
+ return true;
+ }
+
+ @action toggleSort = () => { this.props.CollectionView.props.Document.stackingHeadersSortDescending = !this.props.CollectionView.props.Document.stackingHeadersSortDescending; }
+ @action resetValue = () => { this._currentKey = this.sectionFilter; };
+
+ render() {
+ return (
+ <div className="collectionStackingViewChrome-cont">
+ <button className="collectionStackingViewChrome-sort" onClick={this.toggleSort}>
+ <div className="collectionStackingViewChrome-sortLabel">
+ Sort
+ </div>
+ <div className="collectionStackingViewChrome-sortIcon" style={{ transform: `rotate(${this.descending ? "180" : "0"}deg)` }}>
+ <FontAwesomeIcon icon="caret-up" size="2x" color="white" />
+ </div>
+ </button>
+ <div className="collectionStackingViewChrome-sectionFilter-cont">
+ <div className="collectionStackingViewChrome-sectionFilter-label">
+ GROUP ITEMS BY:
+ </div>
+ <div className="collectionStackingViewChrome-sectionFilter">
+ <EditableView
+ GetValue={() => this.sectionFilter}
+ autosuggestProps={
+ {
+ resetValue: this.resetValue,
+ value: this._currentKey,
+ onChange: this.onKeyChange,
+ autosuggestProps: {
+ inputProps:
+ {
+ value: this._currentKey,
+ onChange: this.onKeyChange
+ },
+ getSuggestionValue: this.getSuggestionValue,
+ suggestions: this.suggestions,
+ alwaysRenderSuggestions: true,
+ renderSuggestion: this.renderSuggestion,
+ onSuggestionsFetchRequested: this.onSuggestionFetch,
+ onSuggestionsClearRequested: this.onSuggestionClear
+ }
+ }}
+ oneLine
+ SetValue={this.setValue}
+ contents={this.sectionFilter ? this.sectionFilter : "N/A"}
+ />
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+
+@observer
+export class CollectionSchemaViewChrome extends React.Component<CollectionViewChromeProps> {
+
+ togglePreview = () => {
+ let dividerWidth = 4;
+ let borderWidth = Number(COLLECTION_BORDER_WIDTH);
+ let panelWidth = this.props.CollectionView.props.PanelWidth();
+ let previewWidth = NumCast(this.props.CollectionView.props.Document.schemaPreviewWidth);
+ let tableWidth = panelWidth - 2 * borderWidth - dividerWidth - previewWidth;
+ this.props.CollectionView.props.Document.schemaPreviewWidth = previewWidth === 0 ? Math.min(tableWidth / 3, 200) : 0;
+
+ }
+
+
+ render() {
+ let previewWidth = NumCast(this.props.CollectionView.props.Document.schemaPreviewWidth);
+ return (
+ <div className="collectionStackingViewChrome-cont">
+ <div id="preview-schema-checkbox-div"><input type="checkbox" key={"Show Preview"} checked={previewWidth !== 0} onChange={this.togglePreview} />Show Preview</div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/KeyRestrictionRow.tsx b/src/client/views/collections/KeyRestrictionRow.tsx
new file mode 100644
index 000000000..8051a8359
--- /dev/null
+++ b/src/client/views/collections/KeyRestrictionRow.tsx
@@ -0,0 +1,45 @@
+import * as React from "react";
+import { observable, runInAction } from "mobx";
+import { observer } from "mobx-react";
+import { PastelSchemaPalette } from "../../../new_fields/SchemaHeaderField";
+
+interface IKeyRestrictionProps {
+ contains: boolean;
+ script: (value: string) => void;
+}
+
+@observer
+export default class KeyRestrictionRow extends React.Component<IKeyRestrictionProps> {
+ @observable private _key = "";
+ @observable private _value = "";
+ @observable private _contains = this.props.contains;
+
+ render() {
+ if (this._key && this._value) {
+ let parsedValue: string | number = `"${this._value}"`;
+ let parsed = parseInt(this._value);
+ if (!isNaN(parsed)) {
+ parsedValue = parsed;
+ }
+ let scriptText = `(doc.${this._key} ${this._contains ? "===" : "!=="} ${parsedValue})`;
+ this.props.script(scriptText);
+ }
+ return (
+ <div className="collectionViewBaseChrome-viewSpecsMenu-row">
+ <input className="collectionViewBaseChrome-viewSpecsMenu-rowLeft"
+ value={this._key}
+ onChange={(e) => runInAction(() => this._key = e.target.value)}
+ placeholder="KEY" />
+ <button className="collectionViewBaseChrome-viewSpecsMenu-rowMiddle"
+ style={{ background: PastelSchemaPalette.get(this._contains ? "green" : "red") }}
+ onClick={() => runInAction(() => this._contains = !this._contains)}>
+ {this._contains ? "CONTAINS" : "DOES NOT CONTAIN"}
+ </button>
+ <input className="collectionViewBaseChrome-viewSpecsMenu-rowRight"
+ value={this._value}
+ onChange={(e) => runInAction(() => this._value = e.target.value)}
+ placeholder="VALUE" />
+ </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 d07fc7f80..d70022280 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,4 +1,4 @@
-import { action, computed } from "mobx";
+import { action, computed, trace } from "mobx";
import { observer } from "mobx-react";
import { Doc, DocListCastAsync, HeightSym, WidthSym, DocListCast } from "../../../../new_fields/Doc";
import { Id } from "../../../../new_fields/FieldSymbols";
@@ -575,7 +575,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
render() {
const easing = () => this.props.Document.panTransformType === "Ease";
-
Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey);
return (
<div className={"collectionfreeformview-container"} ref={this.createDropTarget} onWheel={this.onPointerWheel}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index d96e93aeb..1c767e012 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -19,6 +19,7 @@ import { CollectionViewType } from "../CollectionBaseView";
import { CollectionFreeFormView } from "./CollectionFreeFormView";
import "./MarqueeView.scss";
import React = require("react");
+import { SchemaHeaderField, RandomPastel } from "../../../../new_fields/SchemaHeaderField";
interface MarqueeViewProps {
getContainerTransform: () => Transform;
@@ -134,7 +135,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
doc.width = 200;
docList.push(doc);
}
- let newCol = Docs.Create.SchemaDocument([...(groupAttr ? ["_group"] : []), ...columns.filter(c => c)], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 });
+ let newCol = Docs.Create.SchemaDocument([...(groupAttr ? [new SchemaHeaderField("_group")] : []), ...columns.filter(c => c).map(c => new SchemaHeaderField(c))], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 });
this.props.addDocument(newCol, false);
}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index ddb3ad44e..99f073459 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -643,7 +643,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@computed get nativeWidth() { return this.Document.nativeWidth || 0; }
@computed get nativeHeight() { return this.Document.nativeHeight || 0; }
@computed get contents() {
- return (<DocumentContentsView {...this.props} isSelected={this.isSelected} select={this.select} selectOnLoad={this.props.selectOnLoad} layoutKey={"layout"} DataDoc={this.dataDoc} />);
+ return (<DocumentContentsView {...this.props}
+ isSelected={this.isSelected} select={this.select}
+ selectOnLoad={this.props.selectOnLoad}
+ layoutKey={"layout"}
+ fitToBox={BoolCast(this.props.Document.fitToBox) ? true : this.props.fitToBox}
+ DataDoc={this.dataDoc} />);
}
get layoutDoc() {
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index afde85b69..0ea948c81 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -11,6 +11,7 @@ import { faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, faTim
import { library } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SetupDrag } from "../../util/DragManager";
+import { SchemaHeaderField, RandomPastel } from "../../../new_fields/SchemaHeaderField";
library.add(faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, faTimes, faPlus);
@@ -289,7 +290,7 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
let index = keys.indexOf("");
if (index > -1) keys.splice(index, 1);
- let cols = ["anchor1", "anchor2", ...[...keys]];
+ let cols = ["anchor1", "anchor2", ...[...keys]].map(c => new SchemaHeaderField(c));
let docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType);
let createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" }));
let ref = React.createRef<HTMLDivElement>();
diff --git a/src/client/views/nodes/LinkMenuGroup.tsx b/src/client/views/nodes/LinkMenuGroup.tsx
index ae97bed2f..3637807ad 100644
--- a/src/client/views/nodes/LinkMenuGroup.tsx
+++ b/src/client/views/nodes/LinkMenuGroup.tsx
@@ -14,6 +14,7 @@ import { Docs } from "../../documents/Documents";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { UndoManager } from "../../util/UndoManager";
import { StrCast } from "../../../new_fields/Types";
+import { SchemaHeaderField, RandomPastel } from "../../../new_fields/SchemaHeaderField";
interface LinkMenuGroupProps {
sourceDoc: Doc;
@@ -70,7 +71,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
let index = keys.indexOf("");
if (index > -1) keys.splice(index, 1);
- let cols = ["anchor1", "anchor2", ...[...keys]];
+ let cols = ["anchor1", "anchor2", ...[...keys]].map(c => new SchemaHeaderField(c));
let docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType);
let createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" }));
let ref = React.createRef<HTMLDivElement>();
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index b5708e97b..da4f459e2 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -283,21 +283,28 @@ export namespace Doc {
export function AddDocToList(target: Doc, key: string, doc: Doc, relativeTo?: Doc, before?: boolean, first?: boolean, allowDuplicates?: boolean) {
if (target[key] === undefined) {
+ console.log("target key undefined");
Doc.GetProto(target)[key] = new List<Doc>();
}
let list = Cast(target[key], listSpec(Doc));
if (list) {
+ console.log("has list");
if (allowDuplicates !== true) {
let pind = list.reduce((l, d, i) => d instanceof Doc && Doc.AreProtosEqual(d, doc) ? i : l, -1);
if (pind !== -1) {
list.splice(pind, 1);
}
}
- if (first) list.splice(0, 0, doc);
+ if (first) {
+ console.log("is first");
+ list.splice(0, 0, doc);
+ }
else {
+ console.log("not first");
let ind = relativeTo ? list.indexOf(relativeTo) : -1;
if (ind === -1) list.push(doc);
else list.splice(before ? ind : ind + 1, 0, doc);
+ console.log("index", ind);
}
}
return true;
diff --git a/src/new_fields/SchemaHeaderField.ts b/src/new_fields/SchemaHeaderField.ts
new file mode 100644
index 000000000..a6df31e81
--- /dev/null
+++ b/src/new_fields/SchemaHeaderField.ts
@@ -0,0 +1,87 @@
+import { Deserializable } from "../client/util/SerializationHelper";
+import { serializable, createSimpleSchema, primitive } from "serializr";
+import { ObjectField } from "./ObjectField";
+import { Copy, ToScriptString, OnUpdate } from "./FieldSymbols";
+import { scriptingGlobal, Scripting } from "../client/util/Scripting";
+import { ColumnType } from "../client/views/collections/CollectionSchemaView";
+
+export const PastelSchemaPalette = new Map<string, string>([
+ ["pink1", "#FFB4E8"],
+ ["pink2", "#ff9cee"],
+ ["pink3", "#ffccf9"],
+ ["pink4", "#fcc2ff"],
+ ["pink5", "#f6a6ff"],
+ ["purple1", "#b28dff"],
+ ["purple2", "#c5a3ff"],
+ ["purple3", "#d5aaff"],
+ ["purple4", "#ecd4ff"],
+ ["purple5", "#fb34ff"],
+ ["purple6", "#dcd3ff"],
+ ["purple7", "#a79aff"],
+ ["purple8", "#b5b9ff"],
+ ["purple9", "#97a2ff"],
+ ["bluegreen1", "#afcbff"],
+ ["bluegreen2", "#aff8db"],
+ ["bluegreen3", "#c4faf8"],
+ ["bluegreen4", "#85e3ff"],
+ ["bluegreen5", "#ace7ff"],
+ ["bluegreen6", "#6eb5ff"],
+ ["bluegreen7", "#bffcc6"],
+ ["bluegreen8", "#dbffd6"],
+ ["yellow1", "#f3ffe3"],
+ ["yellow2", "#e7ffac"],
+ ["yellow3", "#ffffd1"],
+ ["yellow4", "#fff5ba"],
+ ["red1", "#ffc9de"],
+ ["red2", "#ffabab"],
+ ["red3", "#ffbebc"],
+ ["red4", "#ffcbc1"],
+]);
+
+export const RandomPastel = () => Array.from(PastelSchemaPalette.values())[Math.floor(Math.random() * PastelSchemaPalette.size)];
+
+@scriptingGlobal
+@Deserializable("schemaheader")
+export class SchemaHeaderField extends ObjectField {
+ @serializable(primitive())
+ heading: string;
+ color: string;
+ type: number;
+
+ constructor(heading: string = "", color: string = RandomPastel(), type?: ColumnType) {
+ super();
+
+ this.heading = heading;
+ this.color = color;
+ if (type) {
+ this.type = type;
+ }
+ else {
+ this.type = 0;
+ }
+ }
+
+ setHeading(heading: string) {
+ this.heading = heading;
+ this[OnUpdate]();
+ }
+
+ setColor(color: string) {
+ this.color = color;
+ this[OnUpdate]();
+ }
+
+ setType(type: ColumnType) {
+ this.type = type;
+ this[OnUpdate]();
+ }
+
+ [Copy]() {
+ return new SchemaHeaderField(this.heading, this.color, this.type);
+ }
+
+ [ToScriptString]() {
+ return `invalid`;
+ }
+}
+
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 1c52a3f11..41eb1aa0c 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -50,6 +50,7 @@ export class CurrentUserUtils {
if (doc.workspaces === undefined) {
const workspaces = Docs.Create.TreeDocument([], { title: "Workspaces", height: 100 });
workspaces.excludeFromLibrary = true;
+ workspaces.chromeStatus = "disabled";
workspaces.workspaceLibrary = true;
workspaces.boxShadow = "0 0";
doc.workspaces = workspaces;
@@ -57,6 +58,7 @@ export class CurrentUserUtils {
if (doc.recentlyClosed === undefined) {
const recentlyClosed = Docs.Create.TreeDocument([], { title: "Recently Closed", height: 75 });
recentlyClosed.excludeFromLibrary = true;
+ recentlyClosed.chromeStatus = "disabled";
recentlyClosed.boxShadow = "0 0";
doc.recentlyClosed = recentlyClosed;
}
@@ -65,11 +67,13 @@ export class CurrentUserUtils {
sidebar.excludeFromLibrary = true;
sidebar.gridGap = 5;
sidebar.xMargin = 5;
+ sidebar.chromeStatus = "disabled";
sidebar.yMargin = 5;
Doc.GetProto(sidebar).backgroundColor = "#aca3a6";
sidebar.boxShadow = "1 1 3";
doc.sidebar = sidebar;
}
+ doc.chromeStatus = "disabled";
StrCast(doc.title).indexOf("@") !== -1 && (doc.title = StrCast(doc.title).split("@")[0] + "'s Library");
}