aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/collections/old_collectionSchema
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/collections/old_collectionSchema')
-rw-r--r--src/client/views/collections/old_collectionSchema/OldCollectionSchemaCells.tsx681
-rw-r--r--src/client/views/collections/old_collectionSchema/OldCollectionSchemaHeaders.tsx510
-rw-r--r--src/client/views/collections/old_collectionSchema/OldCollectionSchemaMovableColumn.tsx138
-rw-r--r--src/client/views/collections/old_collectionSchema/OldCollectionSchemaMovableRow.tsx151
-rw-r--r--src/client/views/collections/old_collectionSchema/OldCollectionSchemaView.scss599
-rw-r--r--src/client/views/collections/old_collectionSchema/OldCollectionSchemaView.tsx649
-rw-r--r--src/client/views/collections/old_collectionSchema/OldSchemaTable.tsx693
7 files changed, 3421 insertions, 0 deletions
diff --git a/src/client/views/collections/old_collectionSchema/OldCollectionSchemaCells.tsx b/src/client/views/collections/old_collectionSchema/OldCollectionSchemaCells.tsx
new file mode 100644
index 000000000..fb93d8b8e
--- /dev/null
+++ b/src/client/views/collections/old_collectionSchema/OldCollectionSchemaCells.tsx
@@ -0,0 +1,681 @@
+import React = require('react');
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import { extname } from 'path';
+import DatePicker from 'react-datepicker';
+import { CellInfo } from 'react-table';
+import { DateField } from '../../../../fields/DateField';
+import { Doc, DocListCast, Field, Opt } from '../../../../fields/Doc';
+import { Id } from '../../../../fields/FieldSymbols';
+import { List } from '../../../../fields/List';
+import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField';
+import { ComputedField } from '../../../../fields/ScriptField';
+import { BoolCast, Cast, DateCast, FieldValue, StrCast } from '../../../../fields/Types';
+import { ImageField } from '../../../../fields/URLField';
+import { emptyFunction, Utils } from '../../../../Utils';
+import { Docs } from '../../../documents/Documents';
+import { DocumentType } from '../../../documents/DocumentTypes';
+import { DocumentManager } from '../../../util/DocumentManager';
+import { DragManager } from '../../../util/DragManager';
+import { KeyCodes } from '../../../util/KeyCodes';
+import { CompileScript } from '../../../util/Scripting';
+import { SearchUtil } from '../../../util/SearchUtil';
+import { SnappingManager } from '../../../util/SnappingManager';
+import { undoBatch } from '../../../util/UndoManager';
+import '../../../views/DocumentDecorations.scss';
+import { EditableView } from '../../EditableView';
+import { MAX_ROW_HEIGHT } from '../../global/globalCssVariables.scss';
+import { DocumentIconContainer } from '../../nodes/DocumentIcon';
+import { OverlayView } from '../../OverlayView';
+import { CollectionView } from '../CollectionView';
+import './CollectionSchemaView.scss';
+
+// intialize cell properties
+export interface CellProps {
+ row: number;
+ col: number;
+ rowProps: CellInfo;
+ // currently unused
+ CollectionView: Opt<CollectionView>;
+ // currently unused
+ ContainingCollection: Opt<CollectionView>;
+ Document: Doc;
+ // column name
+ fieldKey: string;
+ // currently unused
+ renderDepth: number;
+ // called when a button is pressed on the node itself
+ addDocTab: (document: Doc, where: string) => boolean;
+ pinToPres: (document: Doc) => void;
+ moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean;
+ isFocused: boolean;
+ changeFocusedCellByIndex: (row: number, col: number) => void;
+ // set whether the cell is in the isEditing mode
+ setIsEditing: (isEditing: boolean) => void;
+ isEditable: boolean;
+ setPreviewDoc: (doc: Doc) => void;
+ setComputed: (script: string, doc: Doc, field: string, row: number, col: number) => boolean;
+ getField: (row: number, col?: number) => void;
+ // currnetly unused
+ showDoc: (doc: Doc | undefined, dataDoc?: any, screenX?: number, screenY?: number) => void;
+}
+
+@observer
+export class CollectionSchemaCell extends React.Component<CellProps> {
+ // return a field key that is corrected for whether it COMMENT
+ public static resolvedFieldKey(column: string, rowDoc: Doc) {
+ const fieldKey = column;
+ if (fieldKey.startsWith('*')) {
+ const rootKey = fieldKey.substring(1);
+ const allKeys = [...Array.from(Object.keys(rowDoc)), ...Array.from(Object.keys(Doc.GetProto(rowDoc)))];
+ const matchedKeys = allKeys.filter(key => key.includes(rootKey));
+ if (matchedKeys.length) return matchedKeys[0];
+ }
+ return fieldKey;
+ }
+ @observable protected _isEditing: boolean = false;
+ protected _focusRef = React.createRef<HTMLDivElement>();
+ protected _rowDoc = this.props.rowProps.original;
+ // Gets the serialized data in proto form of the base proto that this document's proto inherits from
+ protected _rowDataDoc = Doc.GetProto(this.props.rowProps.original);
+ // methods for dragging and dropping
+ protected _dropDisposer?: DragManager.DragDropDisposer;
+ @observable contents: string = '';
+
+ componentDidMount() {
+ document.addEventListener('keydown', this.onKeyDown);
+ }
+ componentWillUnmount() {
+ document.removeEventListener('keydown', this.onKeyDown);
+ }
+
+ @action
+ onKeyDown = (e: KeyboardEvent): void => {
+ // If a cell is editable and clicked, hitting enter shoudl allow the user to edit it
+ if (this.props.isFocused && this.props.isEditable && e.keyCode === KeyCodes.ENTER) {
+ document.removeEventListener('keydown', this.onKeyDown);
+ this._isEditing = true;
+ this.props.setIsEditing(true);
+ }
+ };
+
+ @action
+ isEditingCallback = (isEditing: boolean): void => {
+ // a general method that takes a boolean that determines whether the cell should be in
+ // is-editing mode
+ // remove the event listener if it's there
+ document.removeEventListener('keydown', this.onKeyDown);
+ // it's not already in is-editing mode, re-add the event listener
+ isEditing && document.addEventListener('keydown', this.onKeyDown);
+ this._isEditing = isEditing;
+ this.props.setIsEditing(isEditing);
+ this.props.changeFocusedCellByIndex(this.props.row, this.props.col);
+ };
+
+ @action
+ onPointerDown = async (e: React.PointerEvent): Promise<void> => {
+ // pan to the cell
+ this.onItemDown(e);
+ // focus on it
+ this.props.changeFocusedCellByIndex(this.props.row, this.props.col);
+ this.props.setPreviewDoc(this.props.rowProps.original);
+
+ let url: string;
+ if ((url = StrCast(this.props.rowProps.row.href))) {
+ // opens up the the doc in a new window, blurring the old one
+ try {
+ new URL(url);
+ const temp = window.open(url)!;
+ temp.blur();
+ window.focus();
+ } catch {}
+ }
+
+ const doc = Cast(this._rowDoc[this.renderFieldKey], Doc, null);
+ doc && this.props.setPreviewDoc(doc);
+ };
+
+ @undoBatch
+ applyToDoc = (doc: Doc, row: number, col: number, run: (args?: { [name: string]: any }) => any) => {
+ // apply a specified change to the cell
+ const res = run({ this: doc, $r: row, $c: col, $: (r: number = 0, c: number = 0) => this.props.getField(r + row, c + col) });
+ if (!res.success) return false;
+ // change what is rendered to this new changed cell content
+ doc[this.renderFieldKey] = res.result;
+ return true;
+ // return whether the change was successful
+ };
+
+ private drop = (e: Event, de: DragManager.DropEvent) => {
+ // if the drag has data at its completion
+ if (de.complete.docDragData) {
+ // if only one doc was dragged
+ if (de.complete.docDragData.draggedDocuments.length === 1) {
+ // update the renderFieldKey
+ this._rowDataDoc[this.renderFieldKey] = de.complete.docDragData.draggedDocuments[0];
+ } else {
+ // create schema document reflecting the new column arrangement
+ const coll = Docs.Create.SchemaDocument([new SchemaHeaderField('title', '#f1efeb')], de.complete.docDragData.draggedDocuments, {});
+ this._rowDataDoc[this.renderFieldKey] = coll;
+ }
+ e.stopPropagation();
+ }
+ };
+
+ protected dropRef = (ele: HTMLElement | null) => {
+ // if the drop disposer is not undefined, run its function
+ this._dropDisposer?.();
+ // if ele is not null, give ele a non-undefined drop disposer
+ ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)));
+ };
+
+ returnHighlights(contents: string, positions?: number[]) {
+ if (positions) {
+ const results = [];
+ StrCast(this.props.Document._searchString);
+ const length = StrCast(this.props.Document._searchString).length;
+ const color = contents ? 'black' : 'grey';
+
+ results.push(
+ <span key="-1" style={{ color }}>
+ {contents?.slice(0, positions[0])}
+ </span>
+ );
+ positions.forEach((num, cur) => {
+ results.push(
+ <span key={'start' + cur} style={{ backgroundColor: '#FFFF00', color }}>
+ {contents?.slice(num, num + length)}
+ </span>
+ );
+ let end = 0;
+ cur === positions.length - 1 ? (end = contents.length) : (end = positions[cur + 1]);
+ results.push(
+ <span key={'end' + cur} style={{ color }}>
+ {contents?.slice(num + length, end)}
+ </span>
+ );
+ });
+ return results;
+ }
+ return <span style={{ color: contents ? 'black' : 'grey' }}>{contents ? contents?.valueOf() : 'undefined'}</span>;
+ }
+
+ @computed get renderFieldKey() {
+ // gets the resolved field key of this cell
+ return CollectionSchemaCell.resolvedFieldKey(this.props.rowProps.column.id!, this.props.rowProps.original);
+ }
+
+ onItemDown = async (e: React.PointerEvent) => {
+ // if the document is a document used to change UI for search results in schema view
+ if (this.props.Document._searchDoc) {
+ const aliasdoc = await SearchUtil.GetAliasesOfDocument(this._rowDataDoc);
+ const targetContext = aliasdoc.length <= 0 ? undefined : Cast(aliasdoc[0].context, Doc, null);
+ // Jump to the this document
+ DocumentManager.Instance.jumpToDocument(this._rowDoc, false, emptyFunction, targetContext ? [targetContext] : [], undefined, undefined, undefined, () => this.props.setPreviewDoc(this._rowDoc));
+ }
+ };
+
+ renderCellWithType(type: string | undefined) {
+ const dragRef: React.RefObject<HTMLDivElement> = React.createRef();
+
+ // the column
+ const fieldKey = this.renderFieldKey;
+ // the exact cell
+ const field = this._rowDoc[fieldKey];
+
+ const onPointerEnter = (e: React.PointerEvent): void => {
+ // e.buttons === 1 means the left moue pointer is down
+ if (e.buttons === 1 && SnappingManager.GetIsDragging() && (type === 'document' || type === undefined)) {
+ dragRef.current!.className = 'collectionSchemaView-cellContainer doc-drag-over';
+ }
+ };
+ const onPointerLeave = (e: React.PointerEvent): void => {
+ // change the class name to indicate that the cell is no longer being dragged
+ dragRef.current!.className = 'collectionSchemaView-cellContainer';
+ };
+
+ let contents = Field.toString(field as Field);
+ // display 2 hyphens instead of a blank box for empty cells
+ contents = contents === '' ? '--' : contents;
+
+ // classname reflects the tatus of the cell
+ 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';
+
+ const positions = [];
+ if (StrCast(this.props.Document._searchString).toLowerCase() !== '') {
+ // term is ...promise pending... if the field is a Promise, otherwise it is the cell's contents
+ let term = field instanceof Promise ? '...promise pending...' : contents.toLowerCase();
+ const search = StrCast(this.props.Document._searchString).toLowerCase();
+ let start = term.indexOf(search);
+ let tally = 0;
+ // if search is found in term
+ if (start !== -1) {
+ positions.push(start);
+ }
+ // if search is found in term, continue finding all instances of search in term
+ while (start < contents?.length && start !== -1) {
+ term = term.slice(start + search.length + 1);
+ tally += start + search.length + 1;
+ start = term.indexOf(search);
+ positions.push(tally + start);
+ }
+ // remove the last position
+ if (positions.length > 1) {
+ positions.pop();
+ }
+ }
+ const placeholder = type === 'number' ? '0' : contents === '' ? '--' : 'undefined';
+ return (
+ <div
+ className="collectionSchemaView-cellContainer"
+ style={{ cursor: field instanceof Doc ? 'grab' : 'auto' }}
+ ref={dragRef}
+ onPointerDown={this.onPointerDown}
+ onClick={action(e => (this._isEditing = true))}
+ onPointerEnter={onPointerEnter}
+ onPointerLeave={onPointerLeave}>
+ <div className={className} ref={this._focusRef} tabIndex={-1}>
+ <div className="collectionSchemaView-cellContents" ref={type === undefined || type === 'document' ? this.dropRef : null}>
+ {!this.props.Document._searchDoc ? (
+ <EditableView
+ editing={this._isEditing}
+ isEditingCallback={this.isEditingCallback}
+ display={'inline'}
+ contents={contents}
+ height={'auto'}
+ maxHeight={Number(MAX_ROW_HEIGHT)}
+ placeholder={placeholder}
+ GetValue={() => {
+ const cfield = ComputedField.WithoutComputed(() => FieldValue(field));
+ const cscript = cfield instanceof ComputedField ? cfield.script.originalScript : undefined;
+ const cfinalScript = cscript?.split('return')[cscript.split('return').length - 1];
+ return cscript ? (cfinalScript?.endsWith(';') ? `:=${cfinalScript?.substring(0, cfinalScript.length - 2)}` : cfinalScript) : Field.IsField(cfield) ? Field.toScriptString(cfield) : '';
+ }}
+ SetValue={action((value: string) => {
+ // sets what is displayed after the user makes an input
+ let retVal = false;
+ if (value.startsWith(':=') || value.startsWith('=:=')) {
+ // decides how to compute a value when given either of the above strings
+ const script = value.substring(value.startsWith('=:=') ? 3 : 2);
+ retVal = this.props.setComputed(script, value.startsWith(':=') ? this._rowDataDoc : this._rowDoc, this.renderFieldKey, this.props.row, this.props.col);
+ } else {
+ // check if the input is a number
+ let inputIsNum = true;
+ for (const s of value) {
+ if (isNaN(parseInt(s)) && !(s === '.') && !(s === ',')) {
+ inputIsNum = false;
+ }
+ }
+ // check if the input is a boolean
+ const inputIsBool: boolean = value === 'false' || value === 'true';
+ // what to do in the case
+ if (!inputIsNum && !inputIsBool && !value.startsWith('=')) {
+ // if it's not a number, it's a string, and should be processed as such
+ // strips the string of quotes when it is edited to prevent quotes form being added to the text automatically
+ // after each edit
+ let valueSansQuotes = value;
+ if (this._isEditing) {
+ const vsqLength = valueSansQuotes.length;
+ // get rid of outer quotes
+ valueSansQuotes = valueSansQuotes.substring(value.startsWith('"') ? 1 : 0, valueSansQuotes.charAt(vsqLength - 1) === '"' ? vsqLength - 1 : vsqLength);
+ }
+ let inputAsString = '"';
+ // escape any quotes in the string
+ for (const i of valueSansQuotes) {
+ if (i === '"') {
+ inputAsString += '\\"';
+ } else {
+ inputAsString += i;
+ }
+ }
+ // add a closing quote
+ inputAsString += '"';
+ //two options here: we can strip off outer quotes or we can figure out what's going on with the script
+ const script = CompileScript(inputAsString, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: 'number', $c: 'number', $: 'any' } });
+ const changeMade = inputAsString.length !== value.length || inputAsString.length - 2 !== value.length;
+ // change it if a change is made, otherwise, just compile using the old cell conetnts
+ script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run));
+ // handle numbers and expressions
+ } else if (inputIsNum || value.startsWith('=')) {
+ //TODO: make accept numbers
+ const inputscript = value.substring(value.startsWith('=') ? 1 : 0);
+ // if commas are not stripped, the parser only considers the numbers after the last comma
+ let inputSansCommas = '';
+ for (const s of inputscript) {
+ if (!(s === ',')) {
+ inputSansCommas += s;
+ }
+ }
+ const script = CompileScript(inputSansCommas, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: 'number', $c: 'number', $: 'any' } });
+ const changeMade = value.length - 2 !== value.length;
+ script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run));
+ // handle booleans
+ } else if (inputIsBool) {
+ const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: 'number', $c: 'number', $: 'any' } });
+ const changeMade = value.length - 2 !== value.length;
+ script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run));
+ }
+ }
+ if (retVal) {
+ this._isEditing = false; // need to set this here. otherwise, the assignment of the field will invalidate & cause render() to be called with the wrong value for 'editing'
+ this.props.setIsEditing(false);
+ }
+ return retVal;
+ })}
+ OnFillDown={async (value: string) => {
+ // computes all of the value preceded by :=
+ const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: 'number', $c: 'number', $: 'any' } });
+ script.compiled &&
+ DocListCast(this.props.Document[this.props.fieldKey]).forEach((doc, i) =>
+ value.startsWith(':=') ? this.props.setComputed(value.substring(2), Doc.GetProto(doc), this.renderFieldKey, i, this.props.col) : this.applyToDoc(Doc.GetProto(doc), i, this.props.col, script.run)
+ );
+ }}
+ />
+ ) : (
+ this.returnHighlights(contents, positions)
+ )}
+ </div>
+ </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 CollectionSchemaDateCell extends CollectionSchemaCell {
+ @computed get _date(): Opt<DateField> {
+ // if the cell is a date field, cast then contents to a date. Otherrwwise, make the contents undefined.
+ return this._rowDoc[this.renderFieldKey] instanceof DateField ? DateCast(this._rowDoc[this.renderFieldKey]) : undefined;
+ }
+
+ @action
+ handleChange = (date: any) => {
+ // const script = CompileScript(date.toString(), { requiredType: "Date", addReturn: true, params: { this: Doc.name } });
+ // if (script.compiled) {
+ // this.applyToDoc(this._document, this.props.row, this.props.col, script.run);
+ // } else {
+ // ^ DateCast is always undefined for some reason, but that is what the field should be set to
+ this._rowDoc[this.renderFieldKey] = new DateField(date as Date);
+ //}
+ };
+
+ render() {
+ return !this.props.isFocused ? (
+ <span onPointerDown={this.onPointerDown}>{this._date ? Field.toString(this._date as Field) : '--'}</span>
+ ) : (
+ <DatePicker selected={this._date?.date || new Date()} onSelect={date => this.handleChange(date)} onChange={date => this.handleChange(date)} />
+ );
+ }
+}
+
+@observer
+export class CollectionSchemaDocCell extends CollectionSchemaCell {
+ _overlayDisposer?: () => void;
+
+ @computed get _doc() {
+ return FieldValue(Cast(this._rowDoc[this.renderFieldKey], Doc));
+ }
+
+ @action
+ onSetValue = (value: string) => {
+ this._doc && (Doc.GetProto(this._doc).title = value);
+
+ const script = CompileScript(value, {
+ addReturn: true,
+ typecheck: true,
+ transformer: DocumentIconContainer.getTransformer(),
+ });
+ // compile the script
+ const results = script.compiled && script.run();
+ // if the script was compiled and run
+ if (results && results.success) {
+ this._rowDoc[this.renderFieldKey] = results.result;
+ return true;
+ }
+ return false;
+ };
+
+ componentWillUnmount() {
+ this.onBlur();
+ }
+
+ onBlur = () => {
+ this._overlayDisposer?.();
+ };
+ onFocus = () => {
+ this.onBlur();
+ this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 });
+ };
+
+ @action
+ isEditingCallback = (isEditing: boolean): void => {
+ // the isEditingCallback from a general CollectionSchemaCell
+ document.removeEventListener('keydown', this.onKeyDown);
+ isEditing && document.addEventListener('keydown', this.onKeyDown);
+ this._isEditing = isEditing;
+ this.props.setIsEditing(isEditing);
+ this.props.changeFocusedCellByIndex(this.props.row, this.props.col);
+ };
+
+ render() {
+ // if there's a doc, render it
+ return !this._doc ? (
+ this.renderCellWithType('document')
+ ) : (
+ <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}>
+ <div className="collectionSchemaView-cellContents-document" style={{ padding: '5.9px' }} ref={this.dropRef} onFocus={this.onFocus} onBlur={this.onBlur}>
+ <EditableView
+ editing={this._isEditing}
+ isEditingCallback={this.isEditingCallback}
+ display={'inline'}
+ contents={this._doc.title || '--'}
+ height={'auto'}
+ maxHeight={Number(MAX_ROW_HEIGHT)}
+ GetValue={() => StrCast(this._doc?.title)}
+ SetValue={action((value: string) => {
+ this.onSetValue(value);
+ return true;
+ })}
+ />
+ </div>
+ <div onClick={() => this._doc && this.props.addDocTab(this._doc, 'add:right')} className="collectionSchemaView-cellContents-docButton">
+ <FontAwesomeIcon icon="external-link-alt" size="lg" />
+ </div>
+ </div>
+ );
+ }
+}
+
+@observer
+export class CollectionSchemaImageCell extends CollectionSchemaCell {
+ choosePath(url: URL) {
+ if (url.protocol === 'data') return url.href; // if the url ises the data protocol, just return the href
+ if (url.href.indexOf(window.location.origin) === -1) return Utils.CorsProxy(url.href); // otherwise, put it through the cors proxy erver
+ if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href; //Why is this here — good question
+
+ const ext = extname(url.href);
+ return url.href.replace(ext, '_o' + ext);
+ }
+
+ render() {
+ const field = Cast(this._rowDoc[this.renderFieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc
+ const alts = DocListCast(this._rowDoc[this.renderFieldKey + '-alternates']); // retrieve alternate documents that may be rendered as alternate images
+ const altpaths = alts
+ .map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url)
+ .filter(url => url)
+ .map(url => this.choosePath(url)); // access the primary layout data of the alternate documents
+ const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths;
+ // If there is a path, follow it; otherwise, follow a link to a default image icon
+ const url = paths.length ? paths : [Utils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png')];
+
+ const aspect = Doc.NativeAspect(this._rowDoc); // aspect ratio
+ let width = Math.min(75, this.props.rowProps.width); // get a with that is no smaller than 75px
+ const height = Math.min(75, width / aspect); // get a height either proportional to that or 75 px
+ width = height * aspect; // increase the width of the image if necessary to maintain proportionality
+
+ const reference = React.createRef<HTMLDivElement>();
+ return (
+ <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}>
+ <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} ref={reference}>
+ <img src={url[0]} width={paths.length ? width : '20px'} height={paths.length ? height : '20px'} />
+ </div>
+ </div>
+ );
+ }
+}
+
+@observer
+export class CollectionSchemaListCell extends CollectionSchemaCell {
+ _overlayDisposer?: () => void;
+
+ @computed get _field() {
+ return this._rowDoc[this.renderFieldKey];
+ }
+ @computed get _optionsList() {
+ return this._field as List<any>;
+ }
+ @observable private _opened = false; // whether the list is opened
+ @observable private _text = 'select an item';
+ @observable private _selectedNum = 0; // the index of the list item selected
+
+ @action
+ onSetValue = (value: string) => {
+ // change if it's a document
+ this._optionsList[this._selectedNum] = this._text = value;
+
+ (this._field as List<any>).splice(this._selectedNum, 1, value);
+ };
+
+ @action
+ onSelected = (element: string, index: number) => {
+ // if an item is selected, the private variables should update to reflect this
+ this._text = element;
+ this._selectedNum = index;
+ };
+
+ onFocus = () => {
+ this._overlayDisposer?.();
+ this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 });
+ };
+
+ render() {
+ const link = false;
+ const reference = React.createRef<HTMLDivElement>();
+
+ // if the list is not opened, don't display it; otherwise, do.
+ if (this._optionsList?.length) {
+ const options = !this._opened ? null : (
+ <div>
+ {this._optionsList.map((element, index) => {
+ const val = Field.toString(element);
+ return (
+ <div className="collectionSchemaView-dropdownOption" key={index} style={{ padding: '6px' }} onPointerDown={e => this.onSelected(StrCast(element), index)}>
+ {val}
+ </div>
+ );
+ })}
+ </div>
+ );
+
+ const plainText = <div style={{ padding: '5.9px' }}>{this._text}</div>;
+ const textarea = (
+ <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} style={{ padding: '5.9px' }} ref={this.dropRef}>
+ <EditableView
+ editing={this._isEditing}
+ isEditingCallback={this.isEditingCallback}
+ display={'inline'}
+ contents={this._text}
+ height={'auto'}
+ maxHeight={Number(MAX_ROW_HEIGHT)}
+ GetValue={() => this._text}
+ SetValue={action((value: string) => {
+ // add special for params
+ this.onSetValue(value);
+ return true;
+ })}
+ />
+ </div>
+ );
+
+ //☰
+ return (
+ <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}>
+ <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} ref={reference}>
+ <div className="collectionSchemaView-dropDownWrapper">
+ <button type="button" className="collectionSchemaView-dropdownButton" style={{ right: 'length', position: 'relative' }} onClick={action(e => (this._opened = !this._opened))}>
+ <FontAwesomeIcon icon={this._opened ? 'caret-up' : 'caret-down'} size="sm" />
+ </button>
+ <div className="collectionSchemaView-dropdownText"> {link ? plainText : textarea} </div>
+ </div>
+ {options}
+ </div>
+ </div>
+ );
+ }
+ return this.renderCellWithType('list');
+ }
+}
+
+@observer
+export class CollectionSchemaCheckboxCell extends CollectionSchemaCell {
+ @computed get _isChecked() {
+ return BoolCast(this._rowDoc[this.renderFieldKey]);
+ }
+
+ render() {
+ const reference = React.createRef<HTMLDivElement>();
+ return (
+ <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}>
+ <input type="checkbox" checked={this._isChecked} onChange={e => (this._rowDoc[this.renderFieldKey] = e.target.checked)} />
+ </div>
+ );
+ }
+}
+
+@observer
+export class CollectionSchemaButtons extends CollectionSchemaCell {
+ // the navigation buttons for schema view when it is used for search.
+ render() {
+ return !this.props.Document._searchDoc || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this._rowDoc.type) as DocumentType) ? (
+ <></>
+ ) : (
+ <div style={{ paddingTop: 8, paddingLeft: 3 }}>
+ <button style={{ padding: 2, left: 77 }} onClick={() => Doc.SearchMatchNext(this._rowDoc, true)}>
+ <FontAwesomeIcon icon="arrow-up" size="sm" />
+ </button>
+ <button style={{ padding: 2 }} onClick={() => Doc.SearchMatchNext(this._rowDoc, false)}>
+ <FontAwesomeIcon icon="arrow-down" size="sm" />
+ </button>
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/collections/old_collectionSchema/OldCollectionSchemaHeaders.tsx b/src/client/views/collections/old_collectionSchema/OldCollectionSchemaHeaders.tsx
new file mode 100644
index 000000000..32283d76c
--- /dev/null
+++ b/src/client/views/collections/old_collectionSchema/OldCollectionSchemaHeaders.tsx
@@ -0,0 +1,510 @@
+// import React = require("react");
+// import { IconProp } from "@fortawesome/fontawesome-svg-core";
+// import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+// import { action, computed, observable, runInAction, trace } from "mobx";
+// import { observer } from "mobx-react";
+// import { Doc, DocListCast, Opt, StrListCast } from "../../../../fields/Doc";
+// import { listSpec } from "../../../../fields/Schema";
+// import { PastelSchemaPalette, SchemaHeaderField } from "../../../../fields/SchemaHeaderField";
+// import { ScriptField } from "../../../../fields/ScriptField";
+// import { Cast, StrCast } from "../../../../fields/Types";
+// import { undoBatch } from "../../../util/UndoManager";
+// import { CollectionView } from "../CollectionView";
+// import { ColumnType } from "./CollectionSchemaView";
+// import "./CollectionSchemaView.scss";
+
+// const higflyout = require("@hig/flyout");
+// export const { anchorPoints } = higflyout;
+// export const Flyout = higflyout.default;
+
+// export interface AddColumnHeaderProps {
+// createColumn: () => void;
+// }
+
+// @observer
+// export class CollectionSchemaAddColumnHeader extends React.Component<AddColumnHeaderProps> {
+// // the button that allows the user to add a column
+// render() {
+// return <button className="add-column" onClick={() => this.props.createColumn()}>
+// <FontAwesomeIcon icon="plus" size="sm" />
+// </button>;
+// }
+// }
+
+// export interface ColumnMenuProps {
+// columnField: SchemaHeaderField;
+// // 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: (column: SchemaHeaderField, type: ColumnType) => void;
+// setColumnSort: (column: SchemaHeaderField, desc: boolean | undefined) => void;
+// anchorPoint?: any;
+// setColumnColor: (column: SchemaHeaderField, color: string) => void;
+// }
+// @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); }
+
+// @action
+// detectClick = (e: PointerEvent) => {
+// !this._node?.contains(e.target as Node) && this.props.setIsEditing(this._isOpen = false);
+// }
+
+// @action
+// toggleIsOpen = (): void => {
+// this.props.setIsEditing(this._isOpen = !this._isOpen);
+// }
+
+// changeColumnType = (type: ColumnType) => {
+// this.props.setColumnType(this.props.columnField, type);
+// }
+
+// changeColumnSort = (desc: boolean | undefined) => {
+// this.props.setColumnSort(this.props.columnField, desc);
+// }
+
+// changeColumnColor = (color: string) => {
+// this.props.setColumnColor(this.props.columnField, color);
+// }
+
+// @action
+// setNode = (node: HTMLDivElement): void => {
+// if (node) {
+// this._node = node;
+// }
+// }
+
+// renderTypes = () => {
+// if (this.props.typeConst) return (null);
+
+// const type = this.props.columnField.type;
+// return (
+// <div className="collectionSchema-headerMenu-group">
+// <label>Column type:</label>
+// <div className="columnMenu-types">
+// <div className={"columnMenu-option" + (type === ColumnType.Any ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Any)}>
+// <FontAwesomeIcon icon={"align-justify"} size="sm" />
+// Any
+// </div>
+// <div className={"columnMenu-option" + (type === ColumnType.Number ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Number)}>
+// <FontAwesomeIcon icon={"hashtag"} size="sm" />
+// Number
+// </div>
+// <div className={"columnMenu-option" + (type === ColumnType.String ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.String)}>
+// <FontAwesomeIcon icon={"font"} size="sm" />
+// Text
+// </div>
+// <div className={"columnMenu-option" + (type === ColumnType.Boolean ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Boolean)}>
+// <FontAwesomeIcon icon={"check-square"} size="sm" />
+// Checkbox
+// </div>
+// <div className={"columnMenu-option" + (type === ColumnType.List ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.List)}>
+// <FontAwesomeIcon icon={"list-ul"} size="sm" />
+// List
+// </div>
+// <div className={"columnMenu-option" + (type === ColumnType.Doc ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Doc)}>
+// <FontAwesomeIcon icon={"file"} size="sm" />
+// Document
+// </div>
+// <div className={"columnMenu-option" + (type === ColumnType.Image ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Image)}>
+// <FontAwesomeIcon icon={"image"} size="sm" />
+// Image
+// </div>
+// <div className={"columnMenu-option" + (type === ColumnType.Date ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Date)}>
+// <FontAwesomeIcon icon={"calendar"} size="sm" />
+// Date
+// </div>
+// </div>
+// </div >
+// );
+// }
+
+// renderSorting = () => {
+// const sort = this.props.columnField.desc;
+// return (
+// <div className="collectionSchema-headerMenu-group">
+// <label>Sort by:</label>
+// <div className="columnMenu-sort">
+// <div className={"columnMenu-option" + (sort === true ? " active" : "")} onClick={() => this.changeColumnSort(true)}>
+// <FontAwesomeIcon icon="sort-amount-down" size="sm" />
+// Sort descending
+// </div>
+// <div className={"columnMenu-option" + (sort === false ? " active" : "")} onClick={() => this.changeColumnSort(false)}>
+// <FontAwesomeIcon icon="sort-amount-up" size="sm" />
+// Sort ascending
+// </div>
+// <div className="columnMenu-option" onClick={() => this.changeColumnSort(undefined)}>
+// <FontAwesomeIcon icon="times" size="sm" />
+// Clear sorting
+// </div>
+// </div>
+// </div>
+// );
+// }
+
+// renderColors = () => {
+// const selected = this.props.columnField.color;
+
+// const pink = PastelSchemaPalette.get("pink2");
+// const purple = PastelSchemaPalette.get("purple2");
+// const blue = PastelSchemaPalette.get("bluegreen1");
+// const yellow = PastelSchemaPalette.get("yellow4");
+// const red = PastelSchemaPalette.get("red2");
+// const gray = "#f1efeb";
+
+// return (
+// <div className="collectionSchema-headerMenu-group">
+// <label>Color:</label>
+// <div className="columnMenu-colors">
+// <div className={"columnMenu-colorPicker" + (selected === pink ? " active" : "")} style={{ backgroundColor: pink }} onClick={() => this.changeColumnColor(pink!)}></div>
+// <div className={"columnMenu-colorPicker" + (selected === purple ? " active" : "")} style={{ backgroundColor: purple }} onClick={() => this.changeColumnColor(purple!)}></div>
+// <div className={"columnMenu-colorPicker" + (selected === blue ? " active" : "")} style={{ backgroundColor: blue }} onClick={() => this.changeColumnColor(blue!)}></div>
+// <div className={"columnMenu-colorPicker" + (selected === yellow ? " active" : "")} style={{ backgroundColor: yellow }} onClick={() => this.changeColumnColor(yellow!)}></div>
+// <div className={"columnMenu-colorPicker" + (selected === red ? " active" : "")} style={{ backgroundColor: red }} onClick={() => this.changeColumnColor(red!)}></div>
+// <div className={"columnMenu-colorPicker" + (selected === gray ? " active" : "")} style={{ backgroundColor: gray }} onClick={() => this.changeColumnColor(gray)}></div>
+// </div>
+// </div>
+// );
+// }
+
+// renderContent = () => {
+// return (
+// <div className="collectionSchema-header-menuOptions">
+// {this.props.onlyShowOptions ? <></> :
+// <>
+// {this.renderTypes()}
+// {this.renderSorting()}
+// {this.renderColors()}
+// <div className="collectionSchema-headerMenu-group">
+// <button onClick={() => this.props.deleteColumn(this.props.columnField.heading)}>Hide 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>
+// );
+// }
+// }
+
+// export interface KeysDropdownProps {
+// keyValue: string;
+// possibleKeys: string[];
+// existingKeys: string[];
+// canAddNew: boolean;
+// addNew: boolean;
+// onSelect: (oldKey: string, newKey: string, addnew: boolean, filter?: string) => void;
+// setIsEditing: (isEditing: boolean) => void;
+// width?: string;
+// docs?: Doc[];
+// Document: Doc;
+// dataDoc: Doc | undefined;
+// fieldKey: string;
+// ContainingCollectionDoc: Doc | undefined;
+// ContainingCollectionView: Opt<CollectionView>;
+// active?: (outsideReaction?: boolean) => boolean | undefined;
+// openHeader: (column: any, screenx: number, screeny: number) => void;
+// col: SchemaHeaderField;
+// icon: IconProp;
+// }
+// @observer
+// export class KeysDropdown extends React.Component<KeysDropdownProps> {
+// @observable private _key: string = this.props.keyValue;
+// @observable private _searchTerm: string = this.props.keyValue + ":";
+// @observable private _isOpen: boolean = false;
+// @observable private _node: HTMLDivElement | null = null;
+// @observable private _inputRef: React.RefObject<HTMLInputElement> = React.createRef();
+
+// @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);
+// }
+
+// @action
+// setNode = (node: HTMLDivElement): void => {
+// if (node) {
+// this._node = node;
+// }
+// }
+
+// componentDidMount() {
+// document.addEventListener("pointerdown", this.detectClick);
+// const filters = Cast(this.props.Document._docFilters, listSpec("string"));
+// if (filters?.some(filter => filter.split(":")[0] === this._key)) {
+// runInAction(() => this.closeResultsVisibility = "contents");
+// }
+// }
+
+// @action
+// detectClick = (e: PointerEvent): void => {
+// if (this._node && this._node.contains(e.target as Node)) {
+// } else {
+// this._isOpen = false;
+// this.props.setIsEditing(false);
+// }
+// }
+
+// private tempfilter: string = "";
+// @undoBatch
+// onKeyDown = (e: React.KeyboardEvent): void => {
+// if (e.key === "Enter") {
+// e.stopPropagation();
+// if (this._searchTerm.includes(":")) {
+// const colpos = this._searchTerm.indexOf(":");
+// const temp = this._searchTerm.slice(colpos + 1, this._searchTerm.length);
+// if (temp === "") {
+// Doc.setDocFilter(this.props.Document, this._key, this.tempfilter, "remove");
+// this.updateFilter();
+// }
+// else {
+// Doc.setDocFilter(this.props.Document, this._key, this.tempfilter, "remove");
+// this.tempfilter = temp;
+// Doc.setDocFilter(this.props.Document, this._key, temp, "check");
+// this.props.col.setColor("green");
+// this.closeResultsVisibility = "contents";
+// }
+// }
+// else {
+// Doc.setDocFilter(this.props.Document, this._key, this.tempfilter, "remove");
+// this.updateFilter();
+// if (this.showKeys.length) {
+// this.onSelect(this.showKeys[0]);
+// } else if (this._searchTerm !== "" && this.props.canAddNew) {
+// this.setSearchTerm(this._searchTerm || this._key);
+// this.onSelect(this._searchTerm);
+// }
+// }
+// }
+// }
+
+// onChange = (val: string): void => {
+// this.setSearchTerm(val);
+// }
+
+// @action
+// onFocus = (e: React.FocusEvent): void => {
+// this._isOpen = true;
+// this.props.setIsEditing(true);
+// }
+
+// @computed get showKeys() {
+// const whitelistKeys = ["context", "author", "*lastModified", "text", "data", "tags", "creationDate"];
+// const keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1);
+// const showKeys = new Set<string>();
+// [...keyOptions, ...whitelistKeys].forEach(key => (!Doc.noviceMode ||
+// whitelistKeys.includes(key)
+// || ((!key.startsWith("_") && key[0] === key[0].toUpperCase()) || key[0] === "#")) ? showKeys.add(key) : null);
+// return Array.from(showKeys.keys()).filter(key => !this._searchTerm || key.includes(this._searchTerm));
+// }
+
+// @computed get renderOptions() {
+// if (!this._isOpen) {
+// this.defaultMenuHeight = 0;
+// return (null);
+// }
+// const options = this.showKeys.map(key => {
+// return <div key={key} className="key-option" style={{
+// border: "1px solid lightgray",
+// width: this.props.width, maxWidth: this.props.width, overflowX: "hidden", background: "white",
+// }}
+// onPointerDown={e => {
+// e.stopPropagation();
+// }}
+// 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 (this._key !== this._searchTerm.slice(0, this._key.length)) {
+// if (this._searchTerm !== "" && this.props.canAddNew) {
+// options.push(<div key={""} className="key-option" style={{
+// border: "1px solid lightgray", width: this.props.width, maxWidth: this.props.width, overflowX: "hidden", background: "white",
+// }}
+// onClick={() => { this.onSelect(this._searchTerm); this.setSearchTerm(""); }}>
+// Create "{this._searchTerm}" key</div>);
+// }
+// }
+
+// if (options.length === 0) {
+// this.defaultMenuHeight = 0;
+// }
+// else {
+// if (this.props.docs) {
+// const panesize = this.props.docs.length * 30;
+// options.length * 20 + 8 - 10 > panesize ? this.defaultMenuHeight = panesize : this.defaultMenuHeight = options.length * 20 + 8;
+// }
+// else {
+// options.length > 5 ? this.defaultMenuHeight = 108 : this.defaultMenuHeight = options.length * 20 + 8;
+// }
+// }
+// return options;
+// }
+
+// @computed get docSafe() { return DocListCast(this.props.dataDoc?.[this.props.fieldKey]); }
+
+// @computed get renderFilterOptions() {
+// if (!this._isOpen || !this.props.dataDoc) {
+// this.defaultMenuHeight = 0;
+// return (null);
+// }
+// const keyOptions: string[] = [];
+// const colpos = this._searchTerm.indexOf(":");
+// const temp = this._searchTerm.slice(colpos + 1, this._searchTerm.length);
+// this.docSafe.forEach(doc => {
+// const key = StrCast(doc[this._key]);
+// if (keyOptions.includes(key) === false && key.includes(temp) && key !== "") {
+// keyOptions.push(key);
+// }
+// });
+
+// const filters = StrListCast(this.props.Document._docFilters);
+// if (filters.some(filter => filter.split(":")[0] === this._key) === false) {
+// this.props.col.setColor("rgb(241, 239, 235)");
+// this.closeResultsVisibility = "none";
+// }
+// for (let i = 0; i < (filters?.length ?? 0) - 1; i++) {
+// if (filters[i] === this.props.col.heading && keyOptions.includes(filters[i].split(":")[1]) === false) {
+// keyOptions.push(filters[i + 1]);
+// }
+// }
+// const options = keyOptions.map(key => {
+// let bool = false;
+// if (filters !== undefined) {
+// const ind = filters.findIndex(filter => filter.split(":")[1] === key);
+// const fields = ind === -1 ? undefined : filters[ind].split(":");
+// bool = fields ? fields[2] === "check" : false;
+// }
+// return <div key={key} className="key-option" style={{
+// paddingLeft: 5, textAlign: "left",
+// width: this.props.width, maxWidth: this.props.width, overflowX: "hidden", background: "white", backgroundColor: "white",
+// }}
+// >
+// <input type="checkbox"
+// onPointerDown={e => e.stopPropagation()}
+// onClick={e => e.stopPropagation()}
+// onChange={action(e => {
+// if (e.target.checked) {
+// Doc.setDocFilter(this.props.Document, this._key, key, "check");
+// this.closeResultsVisibility = "contents";
+// this.props.col.setColor("green");
+// } else {
+// Doc.setDocFilter(this.props.Document, this._key, key, "remove");
+// this.updateFilter();
+// }
+// })}
+// checked={bool}
+// />
+// <span style={{ paddingLeft: 4 }}>
+// {key}
+// </span>
+
+// </div>;
+// });
+// if (options.length === 0) {
+// this.defaultMenuHeight = 0;
+// }
+// else {
+// if (this.props.docs) {
+// const panesize = this.props.docs.length * 30;
+// options.length * 20 + 8 - 10 > panesize ? this.defaultMenuHeight = panesize : this.defaultMenuHeight = options.length * 20 + 8;
+// }
+// else {
+// options.length > 5 ? this.defaultMenuHeight = 108 : this.defaultMenuHeight = options.length * 20 + 8;
+// }
+
+// }
+// return options;
+// }
+
+// @observable defaultMenuHeight = 0;
+
+// updateFilter() {
+// const filters = Cast(this.props.Document._docFilters, listSpec("string"));
+// if (filters === undefined || filters.length === 0 || filters.some(filter => filter.split(":")[0] === this._key) === false) {
+// this.props.col.setColor("rgb(241, 239, 235)");
+// this.closeResultsVisibility = "none";
+// }
+// }
+
+// @computed get scriptField() {
+// const scriptText = "setDocFilter(containingTreeView, heading, this.title, checked)";
+// const script = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name });
+// return script ? () => script : undefined;
+// }
+// filterBackground = () => "rgba(105, 105, 105, 0.432)";
+// @observable filterOpen: boolean | undefined = undefined;
+// closeResultsVisibility: string = "none";
+
+// removeFilters = (e: React.PointerEvent): void => {
+// const keyOptions: string[] = [];
+// this.docSafe.forEach(doc => {
+// const key = StrCast(doc[this._key]);
+// if (keyOptions.includes(key) === false) {
+// keyOptions.push(key);
+// }
+// });
+
+// Doc.setDocFilter(this.props.Document, this._key, "", "remove");
+// this.props.col.setColor("rgb(241, 239, 235)");
+// this.closeResultsVisibility = "none";
+// }
+// render() {
+// return (
+// <div style={{ display: "flex", width: '100%', alignContent: 'center', alignItems: 'center' }} ref={this.setNode}>
+// <div className="schema-icon" onClick={e => { this.props.openHeader(this.props.col, e.clientX, e.clientY); e.stopPropagation(); }}>
+// <FontAwesomeIcon icon={this.props.icon} size="lg" style={{ display: "inline" }} />
+// </div>
+
+// <div className="keys-dropdown" style={{ zIndex: 1, width: this.props.width, maxWidth: this.props.width }}>
+// <input className="keys-search" style={{ width: "100%" }}
+// ref={this._inputRef} type="text"
+// value={this._searchTerm} placeholder="Column key"
+// onKeyDown={this.onKeyDown}
+// onChange={e => this.onChange(e.target.value)}
+// onClick={(e) => { e.stopPropagation(); this._inputRef.current?.focus(); }}
+// onFocus={this.onFocus} ></input>
+// <div style={{ display: this.closeResultsVisibility }}>
+// <FontAwesomeIcon onPointerDown={this.removeFilters} icon={"times-circle"} size="lg"
+// style={{ cursor: "hand", color: "grey", padding: 2, left: -20, top: -1, height: 15, position: "relative" }} />
+// </div>
+// {!this._isOpen ? (null) : <div className="keys-options-wrapper" style={{
+// width: this.props.width, maxWidth: this.props.width, height: "auto",
+// }}>
+// {this._searchTerm.includes(":") ? this.renderFilterOptions : this.renderOptions}
+// </div>}
+// </div >
+// </div>
+// );
+// }
+// }
diff --git a/src/client/views/collections/old_collectionSchema/OldCollectionSchemaMovableColumn.tsx b/src/client/views/collections/old_collectionSchema/OldCollectionSchemaMovableColumn.tsx
new file mode 100644
index 000000000..28d2e6ab1
--- /dev/null
+++ b/src/client/views/collections/old_collectionSchema/OldCollectionSchemaMovableColumn.tsx
@@ -0,0 +1,138 @@
+import React = require('react');
+import { action } from 'mobx';
+import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField';
+import { DragManager } from '../../../util/DragManager';
+import { SnappingManager } from '../../../util/SnappingManager';
+import { Transform } from '../../../util/Transform';
+import './CollectionSchemaView.scss';
+
+export interface MovableColumnProps {
+ columnRenderer: React.ReactNode;
+ columnValue: SchemaHeaderField;
+ allColumns: SchemaHeaderField[];
+ reorderColumns: (toMove: SchemaHeaderField, relativeTo: SchemaHeaderField, before: boolean, columns: SchemaHeaderField[]) => void;
+ ScreenToLocalTransform: () => Transform;
+}
+export class MovableColumn extends React.Component<MovableColumnProps> {
+ // The header of the column
+ private _header?: React.RefObject<HTMLDivElement> = React.createRef();
+ // The container of the function that is responsible for moving the column over to a new plac
+ private _colDropDisposer?: DragManager.DragDropDisposer;
+ // initial column position
+ private _startDragPosition: { x: number; y: number } = { x: 0, y: 0 };
+ // sensitivity to being dragged, in pixels
+ private _sensitivity: number = 16;
+ // Column reference ID
+ private _dragRef: React.RefObject<HTMLDivElement> = React.createRef();
+
+ onPointerEnter = (e: React.PointerEvent): void => {
+ // if the column is left-clicked and it is being dragged
+ if (e.buttons === 1 && SnappingManager.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);
+ !e.buttons && document.removeEventListener('pointermove', this.onPointerMove);
+ };
+
+ onDragMove = (e: PointerEvent): void => {
+ // only take into account the horizonal direction when a column is dragged
+ const x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY);
+ const rect = this._header!.current!.getBoundingClientRect();
+ // Now store the point at the top center of the column when it was in its original position
+ const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left + (rect.right - rect.left) / 2, rect.top);
+ // to be compared with its new horizontal position
+ const 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?.();
+ if (ele) {
+ this._colDropDisposer = DragManager.MakeDropTarget(ele, this.colDrop.bind(this));
+ }
+ };
+
+ colDrop = (e: Event, de: DragManager.DropEvent) => {
+ document.removeEventListener('pointermove', this.onDragMove, true);
+ // we only care about whether the column is shifted to the side
+ const x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y);
+ // get the dimensions of the smallest rectangle that bounds the header
+ const rect = this._header!.current!.getBoundingClientRect();
+ const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left + (rect.right - rect.left) / 2, rect.top);
+ // get whether the column was dragged before or after where it is now
+ const before = x[0] < bounds[0];
+ const colDragData = de.complete.columnDragData;
+ // if there is colDragData, which happen when the drag is complete, reorder the columns according to the established variables
+ if (colDragData) {
+ e.stopPropagation();
+ this.props.reorderColumns(colDragData.colKey, this.props.columnValue, before, this.props.allColumns);
+ return true;
+ }
+ return false;
+ };
+
+ onPointerMove = (e: PointerEvent) => {
+ const onRowMove = (e: PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+
+ document.removeEventListener('pointermove', onRowMove);
+ document.removeEventListener('pointerup', onRowUp);
+ const dragData = new DragManager.ColumnDragData(this.props.columnValue);
+ DragManager.StartColumnDrag(this._dragRef.current!, dragData, e.x, e.y);
+ };
+ const onRowUp = (): void => {
+ document.removeEventListener('pointermove', onRowMove);
+ document.removeEventListener('pointerup', onRowUp);
+ };
+ // if the left mouse button is the one being held
+ if (e.buttons === 1) {
+ const [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y);
+ // If the movemnt of the drag exceeds the sensitivity value
+ if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) {
+ document.removeEventListener('pointermove', this.onPointerMove);
+ e.stopPropagation();
+
+ document.addEventListener('pointermove', onRowMove);
+ document.addEventListener('pointerup', onRowUp);
+ }
+ }
+ };
+
+ onPointerUp = (e: React.PointerEvent) => {
+ document.removeEventListener('pointermove', this.onPointerMove);
+ };
+
+ @action
+ onPointerDown = (e: React.PointerEvent, ref: React.RefObject<HTMLDivElement>) => {
+ this._dragRef = ref;
+ const [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX, e.clientY);
+ // If the cell thing dragged is not being edited
+ if (!(e.target as any)?.tagName.includes('INPUT')) {
+ this._startDragPosition = { x: dx, y: dy };
+ document.addEventListener('pointermove', this.onPointerMove);
+ }
+ };
+
+ render() {
+ const reference = React.createRef<HTMLDivElement>();
+
+ 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={e => this.onPointerDown(e, reference)} onPointerUp={this.onPointerUp}>
+ {this.props.columnRenderer}
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/collections/old_collectionSchema/OldCollectionSchemaMovableRow.tsx b/src/client/views/collections/old_collectionSchema/OldCollectionSchemaMovableRow.tsx
new file mode 100644
index 000000000..f872637e5
--- /dev/null
+++ b/src/client/views/collections/old_collectionSchema/OldCollectionSchemaMovableRow.tsx
@@ -0,0 +1,151 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action } from 'mobx';
+import * as React from 'react';
+import { ReactTableDefaults, RowInfo } from 'react-table';
+import { Doc } from '../../../../fields/Doc';
+import { Cast, FieldValue, StrCast } from '../../../../fields/Types';
+import { DocumentManager } from '../../../util/DocumentManager';
+import { DragManager, dropActionType, SetupDrag } from '../../../util/DragManager';
+import { SnappingManager } from '../../../util/SnappingManager';
+import { Transform } from '../../../util/Transform';
+import { undoBatch } from '../../../util/UndoManager';
+import { ContextMenu } from '../../ContextMenu';
+import './CollectionSchemaView.scss';
+
+export interface MovableRowProps {
+ rowInfo: RowInfo;
+ ScreenToLocalTransform: () => Transform;
+ addDoc: (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => boolean;
+ removeDoc: (doc: Doc | Doc[]) => boolean;
+ rowFocused: boolean;
+ textWrapRow: (doc: Doc) => void;
+ rowWrapped: boolean;
+ dropAction: string;
+ addDocTab: any;
+}
+
+export class MovableRow extends React.Component<React.PropsWithChildren<MovableRowProps>> {
+ private _header?: React.RefObject<HTMLDivElement> = React.createRef();
+ private _rowDropDisposer?: DragManager.DragDropDisposer;
+
+ // Event listeners are only necessary when the user is hovering over the table
+ // Create one when the mouse starts hovering...
+ onPointerEnter = (e: React.PointerEvent): void => {
+ if (e.buttons === 1 && SnappingManager.GetIsDragging()) {
+ this._header!.current!.className = 'collectionSchema-row-wrapper';
+ document.addEventListener('pointermove', this.onDragMove, true);
+ }
+ };
+ // ... and delete it when the mouse leaves
+ onPointerLeave = (e: React.PointerEvent): void => {
+ this._header!.current!.className = 'collectionSchema-row-wrapper';
+ document.removeEventListener('pointermove', this.onDragMove, true);
+ };
+ // The method for the event listener, reorders columns when dragged to their new locations.
+ onDragMove = (e: PointerEvent): void => {
+ const x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY);
+ const rect = this._header!.current!.getBoundingClientRect();
+ const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2);
+ const 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();
+ };
+ componentWillUnmount() {
+ this._rowDropDisposer?.();
+ }
+ //
+ createRowDropTarget = (ele: HTMLDivElement) => {
+ this._rowDropDisposer?.();
+ if (ele) {
+ this._rowDropDisposer = DragManager.MakeDropTarget(ele, this.rowDrop.bind(this));
+ }
+ };
+ // Controls what hppens when a row is dragged and dropped
+ rowDrop = (e: Event, de: DragManager.DropEvent) => {
+ this.onPointerLeave(e as any);
+ const rowDoc = FieldValue(Cast(this.props.rowInfo.original, Doc));
+ if (!rowDoc) return false;
+
+ const x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y);
+ const rect = this._header!.current!.getBoundingClientRect();
+ const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2);
+ const before = x[1] < bounds[1];
+
+ const docDragData = de.complete.docDragData;
+ if (docDragData) {
+ e.stopPropagation();
+ if (docDragData.draggedDocuments[0] === rowDoc) return true;
+ const addDocument = (doc: Doc | Doc[]) => this.props.addDoc(doc, rowDoc, before);
+ const movedDocs = docDragData.draggedDocuments;
+ return docDragData.dropAction || docDragData.userDropAction
+ ? docDragData.droppedDocuments.reduce((added: boolean, d) => this.props.addDoc(d, rowDoc, before) || added, false)
+ : docDragData.moveDocument
+ ? movedDocs.reduce((added: boolean, d) => docDragData.moveDocument?.(d, rowDoc, addDocument) || added, false)
+ : docDragData.droppedDocuments.reduce((added: boolean, d) => this.props.addDoc(d, rowDoc, before), false);
+ }
+ return false;
+ };
+
+ onRowContextMenu = (e: React.MouseEvent): void => {
+ const description = this.props.rowWrapped ? 'Unwrap text on row' : 'Text wrap row';
+ ContextMenu.Instance.addItem({ description: description, event: () => this.props.textWrapRow(this.props.rowInfo.original), icon: 'file-pdf' });
+ };
+
+ @undoBatch
+ @action
+ move: DragManager.MoveFunction = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDoc) => {
+ const targetView = targetCollection && DocumentManager.Instance.getDocumentView(targetCollection);
+ return doc !== targetCollection && doc !== targetView?.props.ContainingCollectionDoc && this.props.removeDoc(doc) && addDoc(doc);
+ };
+
+ @action
+ onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
+ console.log('yes');
+ if (e.key === 'Backspace' || e.key === 'Delete') {
+ undoBatch(() => this.props.removeDoc(this.props.rowInfo.original));
+ }
+ };
+
+ 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 null;
+
+ const reference = React.createRef<HTMLDivElement>();
+ const onItemDown = SetupDrag(reference, () => doc, this.move, StrCast(this.props.dropAction) as dropActionType);
+
+ let className = 'collectionSchema-row';
+ if (this.props.rowFocused) className += ' row-focused';
+ if (this.props.rowWrapped) className += ' row-wrapped';
+
+ return (
+ <div className={className} onKeyPress={this.onKeyDown} ref={this.createRowDropTarget} onContextMenu={this.onRowContextMenu}>
+ <div className="collectionSchema-row-wrapper" onKeyPress={this.onKeyDown} ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
+ <ReactTableDefaults.TrComponent onKeyPress={this.onKeyDown}>
+ <div className="row-dragger">
+ <div className="row-option" onClick={undoBatch(() => 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 className="row-option" onClick={() => this.props.addDocTab(this.props.rowInfo.original, 'add:right')}>
+ <FontAwesomeIcon icon="external-link-alt" size="sm" />
+ </div>
+ </div>
+ {children}
+ </ReactTableDefaults.TrComponent>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/collections/old_collectionSchema/OldCollectionSchemaView.scss b/src/client/views/collections/old_collectionSchema/OldCollectionSchemaView.scss
new file mode 100644
index 000000000..22ce8c8f2
--- /dev/null
+++ b/src/client/views/collections/old_collectionSchema/OldCollectionSchemaView.scss
@@ -0,0 +1,599 @@
+@import '../../global/globalCssVariables.scss';
+// @import '../../../../../node_modules/react-table/react-table.css';
+.collectionSchemaView-container {
+ border-width: $COLLECTION_BORDER_WIDTH;
+ border-color: $medium-gray;
+ border-style: solid;
+ border-radius: $border-radius;
+ box-sizing: border-box;
+ position: relative;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ margin-top: 0;
+ transition: top 0.5s;
+ display: flex;
+ justify-content: space-between;
+ flex-wrap: nowrap;
+ touch-action: none;
+ div {
+ touch-action: none;
+ }
+ .collectionSchemaView-tableContainer {
+ width: 100%;
+ height: 100%;
+ }
+ .collectionSchemaView-dividerDragger {
+ position: relative;
+ height: 100%;
+ width: $SCHEMA_DIVIDER_WIDTH;
+ z-index: 20;
+ right: 0;
+ top: 0;
+ background: gray;
+ cursor: col-resize;
+ }
+ // .documentView-node:first-child {
+ // background: $white;
+ // }
+}
+
+.collectionSchemaView-searchContainer {
+ border-width: $COLLECTION_BORDER_WIDTH;
+ border-color: $medium-gray;
+ border-style: solid;
+ border-radius: $border-radius;
+ box-sizing: border-box;
+ position: relative;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ margin-top: 0;
+ transition: top 0.5s;
+ display: flex;
+ justify-content: space-between;
+ flex-wrap: nowrap;
+ touch-action: none;
+ padding: 2px;
+ div {
+ touch-action: none;
+ }
+ .collectionSchemaView-tableContainer {
+ width: 100%;
+ height: 100%;
+ }
+ .collectionSchemaView-dividerDragger {
+ position: relative;
+ height: 100%;
+ width: 20px;
+ z-index: 20;
+ right: 0;
+ top: 0;
+ background: gray;
+ cursor: col-resize;
+ }
+ // .documentView-node:first-child {
+ // background: $white;
+ // }
+}
+
+.ReactTable {
+ width: 100%;
+ background: white;
+ box-sizing: border-box;
+ border: none !important;
+ float: none !important;
+ .rt-table {
+ height: 100%;
+ display: -webkit-inline-box;
+ direction: ltr;
+ overflow: visible;
+ }
+ .rt-noData {
+ display: none;
+ }
+ .rt-thead {
+ width: 100%;
+ z-index: 100;
+ overflow-y: visible;
+ &.-header {
+ font-size: 12px;
+ height: 30px;
+ box-shadow: none;
+ z-index: 100;
+ overflow-y: visible;
+ }
+ .rt-resizable-header-content {
+ height: 100%;
+ overflow: visible;
+ }
+ .rt-th {
+ padding: 0;
+ border-left: solid 1px $light-gray;
+ }
+ }
+ .rt-th {
+ font-size: 13px;
+ text-align: center;
+ &:last-child {
+ overflow: visible;
+ }
+ }
+ .rt-tbody {
+ width: 100%;
+ direction: rtl;
+ overflow: visible;
+ .rt-td {
+ border-right: 1px solid rgba(0, 0, 0, 0.2);
+ }
+ }
+ .rt-tr-group {
+ direction: ltr;
+ flex: 0 1 auto;
+ min-height: 30px;
+ border: 0 !important;
+ }
+ .rt-tr-group:nth-of-type(even) {
+ direction: ltr;
+ flex: 0 1 auto;
+ min-height: 30px;
+ border: 0 !important;
+ background-color: red;
+ }
+ .rt-tr {
+ width: 100%;
+ min-height: 30px;
+ }
+ .rt-td {
+ padding: 0;
+ font-size: 13px;
+ text-align: center;
+ white-space: nowrap;
+ display: flex;
+ align-items: center;
+ .imageBox-cont {
+ position: relative;
+ max-height: 100%;
+ }
+ .imageBox-cont img {
+ object-fit: contain;
+ max-width: 100%;
+ height: 100%;
+ }
+ .videoBox-cont {
+ object-fit: contain;
+ width: auto;
+ height: 100%;
+ }
+ }
+ .rt-td.rt-expandable {
+ display: flex;
+ align-items: center;
+ height: inherit;
+ }
+ .rt-resizer {
+ width: 8px;
+ right: -4px;
+ }
+ .rt-resizable-header {
+ padding: 0;
+ height: 30px;
+ }
+ .rt-resizable-header:last-child {
+ overflow: visible;
+ .rt-resizer {
+ width: 5px !important;
+ }
+ }
+}
+
+.documentView-node-topmost {
+ text-align: left;
+ transform-origin: center top;
+ display: inline-block;
+}
+
+.collectionSchema-col {
+ height: 100%;
+}
+
+.collectionSchema-header-menu {
+ height: auto;
+ z-index: 100;
+ position: absolute;
+ background: white;
+ padding: 5px;
+ position: fixed;
+ background: white;
+ border: black 1px solid;
+ .collectionSchema-header-toggler {
+ z-index: 100;
+ width: 100%;
+ height: 100%;
+ padding: 4px;
+ letter-spacing: 2px;
+ text-transform: uppercase;
+ svg {
+ margin-right: 4px;
+ }
+ }
+}
+
+.collectionSchemaView-header {
+ height: 100%;
+ color: gray;
+ z-index: 100;
+ overflow-y: visible;
+ display: flex;
+ justify-content: space-between;
+ flex-wrap: wrap;
+}
+
+button.add-column {
+ width: 28px;
+}
+
+.collectionSchemaView-menuOptions-wrapper {
+ background: rgb(241, 239, 235);
+ display: flex;
+ cursor: default;
+ height: 100%;
+ align-content: center;
+ align-items: center;
+}
+
+.collectionSchema-header-menuOptions {
+ color: black;
+ width: 180px;
+ text-align: left;
+ .collectionSchema-headerMenu-group {
+ padding: 7px 0;
+ border-bottom: 1px solid lightgray;
+ cursor: pointer;
+ &:first-child {
+ padding-top: 0;
+ }
+ &:last-child {
+ border: none;
+ text-align: center;
+ padding: 12px 0 0 0;
+ }
+ }
+ label {
+ color: $medium-gray;
+ font-weight: normal;
+ letter-spacing: 2px;
+ text-transform: uppercase;
+ }
+ input {
+ color: black;
+ width: 100%;
+ }
+ .columnMenu-option {
+ cursor: pointer;
+ padding: 3px;
+ background-color: white;
+ transition: background-color 0.2s;
+ &:hover {
+ background-color: $light-gray;
+ }
+ &.active {
+ font-weight: bold;
+ border: 2px solid $light-gray;
+ }
+ svg {
+ color: gray;
+ margin-right: 5px;
+ width: 10px;
+ }
+ }
+
+ .keys-dropdown {
+ position: relative;
+ //width: 100%;
+ background-color: white;
+ input {
+ border: 2px solid $light-gray;
+ padding: 3px;
+ height: 28px;
+ font-weight: bold;
+ letter-spacing: '2px';
+ text-transform: 'uppercase';
+ &:focus {
+ font-weight: normal;
+ }
+ }
+ }
+ .columnMenu-colors {
+ display: flex;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ .columnMenu-colorPicker {
+ cursor: pointer;
+ width: 20px;
+ height: 20px;
+ border-radius: 10px;
+ &.active {
+ border: 2px solid white;
+ box-shadow: 0 0 0 2px lightgray;
+ }
+ }
+ }
+}
+
+.schema-icon {
+ cursor: pointer;
+ width: 25px;
+ height: 25px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ align-content: center;
+ background-color: $medium-blue;
+ color: white;
+ margin-right: 5px;
+ font-size: 10px;
+ border-radius: 3px;
+}
+
+.keys-options-wrapper {
+ position: absolute;
+ text-align: left;
+ height: fit-content;
+ top: 100%;
+ z-index: 21;
+ background-color: #ffffff;
+ box-shadow: 0px 3px 4px rgba(0, 0, 0, 30%);
+ padding: 1px;
+ .key-option {
+ cursor: pointer;
+ color: #000000;
+ width: 100%;
+ height: 25px;
+ font-weight: 400;
+ display: flex;
+ justify-content: left;
+ align-items: center;
+ padding-left: 5px;
+ &:hover {
+ background-color: $light-gray;
+ }
+ }
+}
+
+.collectionSchema-row {
+ height: 100%;
+ background-color: white;
+ &.row-focused .rt-td {
+ background-color: $light-blue; //$light-gray;
+ overflow: visible;
+ }
+ &.row-wrapped {
+ .rt-td {
+ white-space: normal;
+ }
+ }
+ .row-dragger {
+ display: flex;
+ justify-content: space-evenly;
+ width: 58px;
+ position: absolute;
+ /* max-width: 50px; */
+ min-height: 30px;
+ align-items: center;
+ color: lightgray;
+ background-color: white;
+ transition: color 0.1s ease;
+ .row-option {
+ color: black;
+ cursor: pointer;
+ position: relative;
+ transition: color 0.1s ease;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ z-index: 2;
+ border-radius: 3px;
+ padding: 3px;
+ &:hover {
+ background-color: $light-gray;
+ }
+ }
+ }
+ .collectionSchema-row-wrapper {
+ &.row-above {
+ border-top: 1px solid $medium-blue;
+ }
+ &.row-below {
+ border-bottom: 1px solid $medium-blue;
+ }
+ &.row-inside {
+ border: 2px dashed $medium-blue;
+ }
+ .row-dragging {
+ background-color: blue;
+ }
+ }
+}
+
+.collectionSchemaView-cellContainer {
+ width: 100%;
+ height: unset;
+}
+
+.collectionSchemaView-cellContents {
+ width: 100%;
+}
+
+.collectionSchemaView-cellWrapper {
+ display: flex;
+ height: 100%;
+ text-align: left;
+ padding-left: 19px;
+ position: relative;
+ align-items: center;
+ align-content: center;
+ &:focus {
+ outline: none;
+ }
+ &.editing {
+ padding: 0;
+ box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3);
+ transform: scale(1.1);
+ z-index: 40;
+ input {
+ outline: 0;
+ border: none;
+ background-color: $white;
+ width: 100%;
+ height: fit-content;
+ min-height: 26px;
+ }
+ }
+ &.focused {
+ overflow: hidden;
+ &.inactive {
+ border: none;
+ }
+ }
+ p {
+ width: 100%;
+ height: 100%;
+ }
+ &:hover .collectionSchemaView-cellContents-docExpander {
+ display: block;
+ }
+ .collectionSchemaView-cellContents-document {
+ display: inline-block;
+ }
+ .collectionSchemaView-cellContents-docButton {
+ float: right;
+ width: '15px';
+ height: '15px';
+ }
+ .collectionSchemaView-dropdownWrapper {
+ border: grey;
+ border-style: solid;
+ border-width: 1px;
+ height: 30px;
+ .collectionSchemaView-dropdownButton {
+ //display: inline-block;
+ float: left;
+ height: 100%;
+ }
+ .collectionSchemaView-dropdownText {
+ display: inline-block;
+ //float: right;
+ height: 100%;
+ display: 'flex';
+ font-size: 13;
+ justify-content: 'center';
+ align-items: 'center';
+ }
+ }
+ .collectionSchemaView-dropdownContainer {
+ position: absolute;
+ border: 1px solid rgba(0, 0, 0, 0.04);
+ box-shadow: 0 16px 24px 2px rgba(0, 0, 0, 0.14);
+ .collectionSchemaView-dropdownOption:hover {
+ background-color: rgba(0, 0, 0, 0.14);
+ cursor: pointer;
+ }
+ }
+}
+
+.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 {
+ z-index: 100;
+}
+
+.collectionSchemaView-toolbar {
+ height: 30px;
+ display: flex;
+ justify-content: flex-end;
+ padding: 0 10px;
+ border-bottom: 2px solid gray;
+ .collectionSchemaView-toolbar-item {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ }
+}
+
+#preview-schema-checkbox-div {
+ margin-left: 20px;
+ font-size: 12px;
+}
+
+.collectionSchemaView-table {
+ width: 100%;
+ height: 100%;
+ overflow: auto;
+ padding: 3px;
+}
+
+.rt-td.rt-expandable {
+ overflow: visible;
+ position: relative;
+ height: 100%;
+ z-index: 1;
+}
+
+.reactTable-sub {
+ background-color: rgb(252, 252, 252);
+ width: 100%;
+ .rt-thead {
+ display: none;
+ }
+ .row-dragger {
+ background-color: rgb(252, 252, 252);
+ }
+ .rt-table {
+ background-color: rgb(252, 252, 252);
+ }
+ .collectionSchemaView-table {
+ width: 100%;
+ border: solid 1px;
+ overflow: visible;
+ padding: 0px;
+ }
+}
+
+.collectionSchemaView-expander {
+ height: 100%;
+ min-height: 30px;
+ position: absolute;
+ color: gray;
+ width: 20;
+ height: auto;
+ left: 55;
+ svg {
+ position: absolute;
+ top: 50%;
+ left: 10;
+ transform: translate(-50%, -50%);
+ }
+}
+
+.collectionSchemaView-addRow {
+ color: gray;
+ letter-spacing: 2px;
+ text-transform: uppercase;
+ cursor: pointer;
+ font-size: 10.5px;
+ margin-left: 50px;
+ margin-top: 10px;
+}
diff --git a/src/client/views/collections/old_collectionSchema/OldCollectionSchemaView.tsx b/src/client/views/collections/old_collectionSchema/OldCollectionSchemaView.tsx
new file mode 100644
index 000000000..260db4b88
--- /dev/null
+++ b/src/client/views/collections/old_collectionSchema/OldCollectionSchemaView.tsx
@@ -0,0 +1,649 @@
+import React = require('react');
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, observable, untracked } from 'mobx';
+import { observer } from 'mobx-react';
+import Measure from 'react-measure';
+// import { Resize } from 'react-table';
+import { Doc, Opt } from '../../../../fields/Doc';
+import { List } from '../../../../fields/List';
+import { listSpec } from '../../../../fields/Schema';
+import { PastelSchemaPalette, SchemaHeaderField } from '../../../../fields/SchemaHeaderField';
+import { Cast, NumCast } from '../../../../fields/Types';
+import { TraceMobx } from '../../../../fields/util';
+import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils';
+import { DocUtils } from '../../../documents/Documents';
+import { SelectionManager } from '../../../util/SelectionManager';
+import { SnappingManager } from '../../../util/SnappingManager';
+import { Transform } from '../../../util/Transform';
+import { undoBatch } from '../../../util/UndoManager';
+import { ContextMenu } from '../../ContextMenu';
+import { ContextMenuProps } from '../../ContextMenuItem';
+import { COLLECTION_BORDER_WIDTH, SCHEMA_DIVIDER_WIDTH } from '../../global/globalCssVariables.scss';
+import { DocumentView } from '../../nodes/DocumentView';
+import { DefaultStyleProvider } from '../../StyleProvider';
+import { CollectionSubView } from '../CollectionSubView';
+import './CollectionSchemaView.scss';
+// import { SchemaTable } from './SchemaTable';
+// bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657
+
+export enum ColumnType {
+ Any,
+ Number,
+ String,
+ Boolean,
+ Doc,
+ Image,
+ List,
+ Date,
+}
+// 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],
+ ['_curPage', ColumnType.Number],
+ ['_currentTimecode', ColumnType.Number],
+ ['zIndex', ColumnType.Number],
+]);
+
+@observer
+export class CollectionSchemaView extends CollectionSubView() {
+ private _previewCont?: HTMLDivElement;
+
+ @observable _previewDoc: Doc | undefined = undefined;
+ @observable _focusedTable: Doc = this.props.Document;
+ @observable _col: any = '';
+ @observable _menuWidth = 0;
+ @observable _headerOpen = false;
+ @observable _headerIsEditing = false;
+ @observable _menuHeight = 0;
+ @observable _pointerX = 0;
+ @observable _pointerY = 0;
+ @observable _openTypes: boolean = false;
+
+ @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 - Number(SCHEMA_DIVIDER_WIDTH) - this.previewWidth();
+ }
+ @computed get borderWidth() {
+ return Number(COLLECTION_BORDER_WIDTH);
+ }
+ @computed get scale() {
+ return this.props.ScreenToLocalTransform().Scale;
+ }
+ @computed get columns() {
+ return Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []);
+ }
+ set columns(columns: SchemaHeaderField[]) {
+ this.props.Document._schemaHeaders = new List<SchemaHeaderField>(columns);
+ }
+
+ @computed get menuCoordinates() {
+ let searchx = 0;
+ let searchy = 0;
+ if (this.props.Document._searchDoc) {
+ const el = document.getElementsByClassName('collectionSchemaView-searchContainer')[0];
+ if (el !== undefined) {
+ const rect = el.getBoundingClientRect();
+ searchx = rect.x;
+ searchy = rect.y;
+ }
+ }
+ const x = Math.max(0, Math.min(document.body.clientWidth - this._menuWidth, this._pointerX)) - searchx;
+ const y = Math.max(0, Math.min(document.body.clientHeight - this._menuHeight, this._pointerY)) - searchy;
+ return this.props.ScreenToLocalTransform().transformPoint(x, y);
+ }
+
+ get documentKeys() {
+ const docs = this.childDocs;
+ const keys: { [key: string]: boolean } = {};
+ // bcz: ugh. this is untracked since otherwise a large collection of documents will blast the server for all their fields.
+ // then as each document's fields come back, we update the documents _proxies. Each time we do this, the whole schema will be
+ // invalidated and re-rendered. This workaround will inquire all of the document fields before the options button is clicked.
+ // then by the time the options button is clicked, all of the fields should be in place. If a new field is added while this menu
+ // is displayed (unlikely) it won't show up until something else changes.
+ //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.heading] = true));
+ return Array.from(Object.keys(keys));
+ }
+
+ @action setHeaderIsEditing = (isEditing: boolean) => (this._headerIsEditing = isEditing);
+
+ @undoBatch
+ setColumnType = action((columnField: SchemaHeaderField, type: ColumnType): void => {
+ this._openTypes = false;
+ if (columnTypes.get(columnField.heading)) return;
+
+ const columns = this.columns;
+ const index = columns.indexOf(columnField);
+ if (index > -1) {
+ columnField.setType(NumCast(type));
+ columns[index] = columnField;
+ this.columns = columns;
+ }
+ });
+
+ @undoBatch
+ setColumnColor = (columnField: SchemaHeaderField, color: string): void => {
+ const columns = this.columns;
+ const index = columns.indexOf(columnField);
+ if (index > -1) {
+ columnField.setColor(color);
+ columns[index] = columnField;
+ this.columns = columns; // need to set the columns to trigger rerender
+ }
+ };
+
+ @undoBatch
+ @action
+ setColumnSort = (columnField: SchemaHeaderField, descending: boolean | undefined) => {
+ const columns = this.columns;
+ columns.forEach(col => col.setDesc(undefined));
+
+ const index = columns.findIndex(c => c.heading === columnField.heading);
+ const column = columns[index];
+ column.setDesc(descending);
+ columns[index] = column;
+ this.columns = columns;
+ };
+
+ renderTypes = (col: any) => {
+ if (columnTypes.get(col.heading)) return null;
+
+ const type = col.type;
+
+ const anyType = (
+ <div className={'columnMenu-option' + (type === ColumnType.Any ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Any)}>
+ <FontAwesomeIcon icon={'align-justify'} size="sm" />
+ Any
+ </div>
+ );
+
+ const numType = (
+ <div className={'columnMenu-option' + (type === ColumnType.Number ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Number)}>
+ <FontAwesomeIcon icon={'hashtag'} size="sm" />
+ Number
+ </div>
+ );
+
+ const textType = (
+ <div className={'columnMenu-option' + (type === ColumnType.String ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.String)}>
+ <FontAwesomeIcon icon={'font'} size="sm" />
+ Text
+ </div>
+ );
+
+ const boolType = (
+ <div className={'columnMenu-option' + (type === ColumnType.Boolean ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Boolean)}>
+ <FontAwesomeIcon icon={'check-square'} size="sm" />
+ Checkbox
+ </div>
+ );
+
+ const listType = (
+ <div className={'columnMenu-option' + (type === ColumnType.List ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.List)}>
+ <FontAwesomeIcon icon={'list-ul'} size="sm" />
+ List
+ </div>
+ );
+
+ const docType = (
+ <div className={'columnMenu-option' + (type === ColumnType.Doc ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Doc)}>
+ <FontAwesomeIcon icon={'file'} size="sm" />
+ Document
+ </div>
+ );
+
+ const imageType = (
+ <div className={'columnMenu-option' + (type === ColumnType.Image ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Image)}>
+ <FontAwesomeIcon icon={'image'} size="sm" />
+ Image
+ </div>
+ );
+
+ const dateType = (
+ <div className={'columnMenu-option' + (type === ColumnType.Date ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Date)}>
+ <FontAwesomeIcon icon={'calendar'} size="sm" />
+ Date
+ </div>
+ );
+
+ const allColumnTypes = (
+ <div className="columnMenu-types">
+ {anyType}
+ {numType}
+ {textType}
+ {boolType}
+ {listType}
+ {docType}
+ {imageType}
+ {dateType}
+ </div>
+ );
+
+ const justColType =
+ type === ColumnType.Any
+ ? anyType
+ : type === ColumnType.Number
+ ? numType
+ : type === ColumnType.String
+ ? textType
+ : type === ColumnType.Boolean
+ ? boolType
+ : type === ColumnType.List
+ ? listType
+ : type === ColumnType.Doc
+ ? docType
+ : type === ColumnType.Date
+ ? dateType
+ : imageType;
+
+ return (
+ <div className="collectionSchema-headerMenu-group" onClick={action(() => (this._openTypes = !this._openTypes))}>
+ <div>
+ <label style={{ cursor: 'pointer' }}>Column type:</label>
+ <FontAwesomeIcon icon={'caret-down'} size="lg" style={{ float: 'right', transform: `rotate(${this._openTypes ? '180deg' : 0})`, transition: '0.2s all ease' }} />
+ </div>
+ {this._openTypes ? allColumnTypes : justColType}
+ </div>
+ );
+ };
+
+ renderSorting = (col: any) => {
+ const sort = col.desc;
+ return (
+ <div className="collectionSchema-headerMenu-group">
+ <label>Sort by:</label>
+ <div className="columnMenu-sort">
+ <div className={'columnMenu-option' + (sort === true ? ' active' : '')} onClick={() => this.setColumnSort(col, true)}>
+ <FontAwesomeIcon icon="sort-amount-down" size="sm" />
+ Sort descending
+ </div>
+ <div className={'columnMenu-option' + (sort === false ? ' active' : '')} onClick={() => this.setColumnSort(col, false)}>
+ <FontAwesomeIcon icon="sort-amount-up" size="sm" />
+ Sort ascending
+ </div>
+ <div className="columnMenu-option" onClick={() => this.setColumnSort(col, undefined)}>
+ <FontAwesomeIcon icon="times" size="sm" />
+ Clear sorting
+ </div>
+ </div>
+ </div>
+ );
+ };
+
+ renderColors = (col: any) => {
+ const selected = col.color;
+
+ const pink = PastelSchemaPalette.get('pink2');
+ const purple = PastelSchemaPalette.get('purple2');
+ const blue = PastelSchemaPalette.get('bluegreen1');
+ const yellow = PastelSchemaPalette.get('yellow4');
+ const red = PastelSchemaPalette.get('red2');
+ const gray = '#f1efeb';
+
+ return (
+ <div className="collectionSchema-headerMenu-group">
+ <label>Color:</label>
+ <div className="columnMenu-colors">
+ <div className={'columnMenu-colorPicker' + (selected === pink ? ' active' : '')} style={{ backgroundColor: pink }} onClick={() => this.setColumnColor(col, pink!)}></div>
+ <div className={'columnMenu-colorPicker' + (selected === purple ? ' active' : '')} style={{ backgroundColor: purple }} onClick={() => this.setColumnColor(col, purple!)}></div>
+ <div className={'columnMenu-colorPicker' + (selected === blue ? ' active' : '')} style={{ backgroundColor: blue }} onClick={() => this.setColumnColor(col, blue!)}></div>
+ <div className={'columnMenu-colorPicker' + (selected === yellow ? ' active' : '')} style={{ backgroundColor: yellow }} onClick={() => this.setColumnColor(col, yellow!)}></div>
+ <div className={'columnMenu-colorPicker' + (selected === red ? ' active' : '')} style={{ backgroundColor: red }} onClick={() => this.setColumnColor(col, red!)}></div>
+ <div className={'columnMenu-colorPicker' + (selected === gray ? ' active' : '')} style={{ backgroundColor: gray }} onClick={() => this.setColumnColor(col, gray)}></div>
+ </div>
+ </div>
+ );
+ };
+
+ @undoBatch
+ @action
+ changeColumns = (oldKey: string, newKey: string, addNew: boolean, filter?: string) => {
+ const columns = this.columns;
+ if (columns === undefined) {
+ this.columns = new List<SchemaHeaderField>([new SchemaHeaderField(newKey, 'f1efeb')]);
+ } else {
+ if (addNew) {
+ columns.push(new SchemaHeaderField(newKey, 'f1efeb'));
+ this.columns = columns;
+ } else {
+ const index = columns.map(c => c.heading).indexOf(oldKey);
+ if (index > -1) {
+ const column = columns[index];
+ column.setHeading(newKey);
+ columns[index] = column;
+ this.columns = columns;
+ if (filter) {
+ Doc.setDocFilter(this.props.Document, newKey, filter, 'match');
+ } else {
+ this.props.Document._docFilters = undefined;
+ }
+ }
+ }
+ }
+ };
+
+ @action
+ openHeader = (col: any, screenx: number, screeny: number) => {
+ this._col = col;
+ this._headerOpen = true;
+ this._pointerX = screenx;
+ this._pointerY = screeny;
+ };
+
+ @action
+ closeHeader = () => {
+ this._headerOpen = false;
+ };
+
+ @undoBatch
+ @action
+ deleteColumn = (key: string) => {
+ const columns = this.columns;
+ if (columns === undefined) {
+ this.columns = new List<SchemaHeaderField>([]);
+ } else {
+ const index = columns.map(c => c.heading).indexOf(key);
+ if (index > -1) {
+ columns.splice(index, 1);
+ this.columns = columns;
+ }
+ }
+ this.closeHeader();
+ };
+
+ getPreviewTransform = (): Transform => {
+ return this.props.ScreenToLocalTransform().translate(-this.borderWidth - NumCast(COLLECTION_BORDER_WIDTH) - this.tableWidth, -this.borderWidth);
+ };
+
+ @action
+ onHeaderClick = (e: React.PointerEvent) => {
+ e.stopPropagation();
+ };
+
+ @action
+ onWheel(e: React.WheelEvent) {
+ const scale = this.props.ScreenToLocalTransform().Scale;
+ this.props.isContentActive(true) && e.stopPropagation();
+ }
+
+ @computed get renderMenuContent() {
+ TraceMobx();
+ return (
+ <div className="collectionSchema-header-menuOptions">
+ {this.renderTypes(this._col)}
+ {this.renderColors(this._col)}
+ <div className="collectionSchema-headerMenu-group">
+ <button
+ onClick={() => {
+ this.deleteColumn(this._col.heading);
+ }}>
+ Hide Column
+ </button>
+ </div>
+ </div>
+ );
+ }
+
+ private createTarget = (ele: HTMLDivElement) => {
+ this._previewCont = ele;
+ super.CreateDropTarget(ele);
+ };
+
+ isFocused = (doc: Doc, outsideReaction: boolean): boolean => this.props.isSelected(outsideReaction) && doc === this._focusedTable;
+
+ @action setFocused = (doc: Doc) => (this._focusedTable = doc);
+
+ @action setPreviewDoc = (doc: Opt<Doc>) => {
+ SelectionManager.SelectSchemaViewDoc(doc);
+ 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) => {
+ setupMoveUpEvents(this, e, this.onDividerMove, emptyFunction, this.toggleExpander);
+ };
+ @action
+ onDividerMove = (e: PointerEvent, down: number[], delta: number[]) => {
+ const nativeWidth = this._previewCont!.getBoundingClientRect();
+ const minWidth = 40;
+ const maxWidth = 1000;
+ const movedWidth = this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0];
+ const width = movedWidth < minWidth ? minWidth : movedWidth > maxWidth ? maxWidth : movedWidth;
+ this.props.Document.schemaPreviewWidth = width;
+ return false;
+ };
+
+ onPointerDown = (e: React.PointerEvent): void => {
+ if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) {
+ if (this.props.isSelected(true)) e.stopPropagation();
+ else this.props.select(false);
+ }
+ };
+
+ @computed
+ get previewDocument(): Doc | undefined {
+ return this._previewDoc;
+ }
+
+ @computed
+ get dividerDragger() {
+ return this.previewWidth() === 0 ? null : (
+ <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown}>
+ <div className="collectionSchemaView-dividerDragger" />
+ </div>
+ );
+ }
+
+ @computed
+ get previewPanel() {
+ return (
+ <div ref={this.createTarget} style={{ width: `${this.previewWidth()}px` }}>
+ {!this.previewDocument ? null : (
+ <DocumentView
+ Document={this.previewDocument}
+ DataDoc={undefined}
+ fitContentsToBox={returnTrue}
+ dontCenter={'y'}
+ focus={DocUtils.DefaultFocus}
+ renderDepth={this.props.renderDepth}
+ rootSelected={this.rootSelected}
+ PanelWidth={this.previewWidth}
+ PanelHeight={this.previewHeight}
+ isContentActive={returnTrue}
+ isDocumentActive={returnFalse}
+ ScreenToLocalTransform={this.getPreviewTransform}
+ docFilters={this.childDocFilters}
+ docRangeFilters={this.childDocRangeFilters}
+ searchFilterDocs={this.searchFilterDocs}
+ styleProvider={DefaultStyleProvider}
+ docViewPath={returnEmptyDoclist}
+ ContainingCollectionDoc={this.props.CollectionView?.props.Document}
+ ContainingCollectionView={this.props.CollectionView}
+ moveDocument={this.props.moveDocument}
+ addDocument={this.props.addDocument}
+ removeDocument={this.props.removeDocument}
+ whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
+ addDocTab={this.props.addDocTab}
+ pinToPres={this.props.pinToPres}
+ bringToFront={returnFalse}
+ />
+ )}
+ </div>
+ );
+ }
+
+ @computed
+ get schemaTable() {
+ return (
+ <SchemaTable
+ Document={this.props.Document}
+ PanelHeight={this.props.PanelHeight}
+ PanelWidth={this.props.PanelWidth}
+ childDocs={this.childDocs}
+ CollectionView={this.props.CollectionView}
+ ContainingCollectionView={this.props.ContainingCollectionView}
+ ContainingCollectionDoc={this.props.ContainingCollectionDoc}
+ fieldKey={this.props.fieldKey}
+ renderDepth={this.props.renderDepth}
+ moveDocument={this.props.moveDocument}
+ ScreenToLocalTransform={this.props.ScreenToLocalTransform}
+ active={this.props.isContentActive}
+ onDrop={this.onExternalDrop}
+ addDocTab={this.props.addDocTab}
+ pinToPres={this.props.pinToPres}
+ isSelected={this.props.isSelected}
+ isFocused={this.isFocused}
+ setFocused={this.setFocused}
+ setPreviewDoc={this.setPreviewDoc}
+ deleteDocument={this.props.removeDocument}
+ addDocument={this.props.addDocument}
+ dataDoc={this.props.DataDoc}
+ columns={this.columns}
+ documentKeys={this.documentKeys}
+ headerIsEditing={this._headerIsEditing}
+ openHeader={this.openHeader}
+ onClick={this.onTableClick}
+ onPointerDown={emptyFunction}
+ onResizedChange={this.onResizedChange}
+ setColumns={this.setColumns}
+ reorderColumns={this.reorderColumns}
+ changeColumns={this.changeColumns}
+ setHeaderIsEditing={this.setHeaderIsEditing}
+ changeColumnSort={this.setColumnSort}
+ />
+ );
+ }
+
+ @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>
+ );
+ }
+
+ onSpecificMenu = (e: React.MouseEvent) => {
+ if ((e.target as any)?.className?.includes?.('collectionSchemaView-cell') || e.target instanceof HTMLSpanElement) {
+ const cm = ContextMenu.Instance;
+ const options = cm.findByDescription('Options...');
+ const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : [];
+ optionItems.push({ description: 'remove', event: () => this._previewDoc && this.props.removeDocument?.(this._previewDoc), icon: 'trash' });
+ !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' });
+ cm.displayMenu(e.clientX, e.clientY);
+ (e.nativeEvent as any).SchemaHandled = true; // not sure why this is needed, but if you right-click quickly on a cell, the Document/Collection contextMenu handlers still fire without this.
+ e.stopPropagation();
+ }
+ };
+
+ @action
+ onTableClick = (e: React.MouseEvent): void => {
+ if (!(e.target as any)?.className?.includes?.('collectionSchemaView-cell') && !(e.target instanceof HTMLSpanElement)) {
+ this.setPreviewDoc(undefined);
+ } else {
+ e.stopPropagation();
+ }
+ this.setFocused(this.props.Document);
+ this.closeHeader();
+ };
+
+ onResizedChange = (newResized: Resize[], event: any) => {
+ const columns = this.columns;
+ newResized.forEach(resized => {
+ const index = columns.findIndex(c => c.heading === resized.id);
+ const column = columns[index];
+ column.setWidth(resized.value);
+ columns[index] = column;
+ });
+ this.columns = columns;
+ };
+
+ @action
+ setColumns = (columns: SchemaHeaderField[]) => (this.columns = columns);
+
+ @undoBatch
+ reorderColumns = (toMove: SchemaHeaderField, relativeTo: SchemaHeaderField, before: boolean, columnsValues: SchemaHeaderField[]) => {
+ const columns = [...columnsValues];
+ const oldIndex = columns.indexOf(toMove);
+ const relIndex = columns.indexOf(relativeTo);
+ const 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.columns = columns;
+ };
+
+ onZoomMenu = (e: React.WheelEvent) => this.props.isContentActive(true) && e.stopPropagation();
+
+ render() {
+ TraceMobx();
+ if (!this.props.isContentActive()) setTimeout(() => this.closeHeader(), 0);
+ const menuContent = this.renderMenuContent;
+ const menu = (
+ <div className="collectionSchema-header-menu" onWheel={e => this.onZoomMenu(e)} onPointerDown={e => this.onHeaderClick(e)} style={{ transform: `translate(${this.menuCoordinates[0]}px, ${this.menuCoordinates[1]}px)` }}>
+ <Measure
+ offset
+ onResize={action((r: any) => {
+ const dim = this.props.ScreenToLocalTransform().inverse().transformDirection(r.offset.width, r.offset.height);
+ this._menuWidth = dim[0];
+ this._menuHeight = dim[1];
+ })}>
+ {({ measureRef }) => <div ref={measureRef}> {menuContent} </div>}
+ </Measure>
+ </div>
+ );
+ return (
+ <div
+ className={'collectionSchemaView' + (this.props.Document._searchDoc ? '-searchContainer' : '-container')}
+ style={{
+ overflow: this.props.scrollOverflow === true ? 'scroll' : undefined,
+ backgroundColor: 'white',
+ pointerEvents: this.props.Document._searchDoc !== undefined && !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'none' : undefined,
+ width: this.props.PanelWidth() || '100%',
+ height: this.props.PanelHeight() || '100%',
+ position: 'relative',
+ }}>
+ <div
+ className="collectionSchemaView-tableContainer"
+ style={{ width: `calc(100% - ${this.previewWidth()}px)` }}
+ onContextMenu={this.onSpecificMenu}
+ onPointerDown={this.onPointerDown}
+ onWheel={e => this.props.isContentActive(true) && e.stopPropagation()}
+ onDrop={e => this.onExternalDrop(e, {})}
+ ref={this.createTarget}>
+ {this.schemaTable}
+ </div>
+ {this.dividerDragger}
+ {!this.previewWidth() ? null : this.previewPanel}
+ {this._headerOpen && this.props.isContentActive() ? menu : null}
+ </div>
+ );
+ TraceMobx();
+ return <div>HELLO</div>;
+ }
+}
diff --git a/src/client/views/collections/old_collectionSchema/OldSchemaTable.tsx b/src/client/views/collections/old_collectionSchema/OldSchemaTable.tsx
new file mode 100644
index 000000000..dfeee3173
--- /dev/null
+++ b/src/client/views/collections/old_collectionSchema/OldSchemaTable.tsx
@@ -0,0 +1,693 @@
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import ReactTable, { CellInfo, Column, ComponentPropsGetterR, Resize, SortingRule } from 'react-table';
+import { DateField } from '../../../../fields/DateField';
+import { AclPrivate, AclReadonly, DataSym, Doc, DocListCast, Field, Opt } from '../../../../fields/Doc';
+import { Id } from '../../../../fields/FieldSymbols';
+import { List } from '../../../../fields/List';
+import { listSpec } from '../../../../fields/Schema';
+import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField';
+import { ComputedField } from '../../../../fields/ScriptField';
+import { Cast, FieldValue, NumCast, StrCast } from '../../../../fields/Types';
+import { ImageField } from '../../../../fields/URLField';
+import { GetEffectiveAcl } from '../../../../fields/util';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../../Utils';
+import { Docs, DocumentOptions, DocUtils } from '../../../documents/Documents';
+import { DocumentType } from '../../../documents/DocumentTypes';
+import { CompileScript, Transformer, ts } from '../../../util/Scripting';
+import { Transform } from '../../../util/Transform';
+import { undoBatch } from '../../../util/UndoManager';
+import '../../../views/DocumentDecorations.scss';
+import { ContextMenu } from '../../ContextMenu';
+import { COLLECTION_BORDER_WIDTH, SCHEMA_DIVIDER_WIDTH } from '../../global/globalCssVariables.scss';
+import { DocumentView } from '../../nodes/DocumentView';
+import { DefaultStyleProvider } from '../../StyleProvider';
+import { CollectionView } from '../CollectionView';
+import {
+ CellProps,
+ CollectionSchemaButtons,
+ CollectionSchemaCell,
+ CollectionSchemaCheckboxCell,
+ CollectionSchemaDateCell,
+ CollectionSchemaDocCell,
+ CollectionSchemaImageCell,
+ CollectionSchemaListCell,
+ CollectionSchemaNumberCell,
+ CollectionSchemaStringCell,
+} from './CollectionSchemaCells';
+import { CollectionSchemaAddColumnHeader, KeysDropdown } from './CollectionSchemaHeaders';
+import { MovableColumn } from './OldCollectionSchemaMovableColumn';
+import { MovableRow } from './CollectionSchemaMovableRow';
+import './CollectionSchemaView.scss';
+
+enum ColumnType {
+ Any,
+ Number,
+ String,
+ Boolean,
+ Doc,
+ Image,
+ List,
+ Date,
+}
+
+// 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],
+ ['_curPage', ColumnType.Number],
+ ['_currentTimecode', ColumnType.Number],
+ ['zIndex', ColumnType.Number],
+]);
+
+export interface SchemaTableProps {
+ Document: Doc; // child doc
+ dataDoc?: Doc;
+ PanelHeight: () => number;
+ PanelWidth: () => number;
+ childDocs?: Doc[];
+ CollectionView: Opt<CollectionView>;
+ ContainingCollectionView: Opt<CollectionView>;
+ ContainingCollectionDoc: Opt<Doc>;
+ fieldKey: string;
+ renderDepth: number;
+ deleteDocument?: (document: Doc | Doc[]) => boolean;
+ addDocument?: (document: Doc | Doc[]) => boolean;
+ moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean;
+ ScreenToLocalTransform: () => Transform;
+ active: (outsideReaction: boolean | undefined) => boolean | undefined;
+ onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void;
+ addDocTab: (document: Doc, where: string) => boolean;
+ pinToPres: (document: Doc) => void;
+ isSelected: (outsideReaction?: boolean) => boolean;
+ isFocused: (document: Doc, outsideReaction: boolean) => boolean;
+ setFocused: (document: Doc) => void;
+ setPreviewDoc: (document: Opt<Doc>) => void;
+ columns: SchemaHeaderField[];
+ documentKeys: any[];
+ headerIsEditing: boolean;
+ openHeader: (column: any, screenx: number, screeny: number) => void;
+ onClick: (e: React.MouseEvent) => void;
+ onPointerDown: (e: React.PointerEvent) => void;
+ onResizedChange: (newResized: Resize[], event: any) => void;
+ setColumns: (columns: SchemaHeaderField[]) => void;
+ reorderColumns: (toMove: SchemaHeaderField, relativeTo: SchemaHeaderField, before: boolean, columnsValues: SchemaHeaderField[]) => void;
+ changeColumns: (oldKey: string, newKey: string, addNew: boolean) => void;
+ setHeaderIsEditing: (isEditing: boolean) => void;
+ changeColumnSort: (columnField: SchemaHeaderField, descending: boolean | undefined) => void;
+}
+
+@observer
+export class SchemaTable extends React.Component<SchemaTableProps> {
+ @observable _cellIsEditing: boolean = false;
+ @observable _focusedCell: { row: number; col: number } = { row: 0, col: 0 };
+ @observable _openCollections: Set<number> = new Set();
+
+ @observable _showDoc: Doc | undefined;
+ @observable _showDataDoc: any = '';
+ @observable _showDocPos: number[] = [];
+
+ @observable _showTitleDropdown: boolean = false;
+
+ @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 - Number(SCHEMA_DIVIDER_WIDTH) - this.previewWidth();
+ }
+
+ @computed get childDocs() {
+ if (this.props.childDocs) return this.props.childDocs;
+
+ const doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document;
+ return DocListCast(doc[this.props.fieldKey]);
+ }
+ set childDocs(docs: Doc[]) {
+ const doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document;
+ doc[this.props.fieldKey] = new List<Doc>(docs);
+ }
+
+ @computed get textWrappedRows() {
+ return Cast(this.props.Document.textwrappedSchemaRows, listSpec('string'), []);
+ }
+ set textWrappedRows(textWrappedRows: string[]) {
+ this.props.Document.textwrappedSchemaRows = new List<string>(textWrappedRows);
+ }
+
+ @computed get resized(): { id: string; value: number }[] {
+ return this.props.columns.reduce((resized, shf) => {
+ shf.width > -1 && resized.push({ id: shf.heading, value: shf.width });
+ return resized;
+ }, [] as { id: string; value: number }[]);
+ }
+ @computed get sorted(): SortingRule[] {
+ return this.props.columns.reduce((sorted, shf) => {
+ shf.desc !== undefined && sorted.push({ id: shf.heading, desc: shf.desc });
+ return sorted;
+ }, [] as SortingRule[]);
+ }
+
+ @action
+ changeSorting = (col: any) => {
+ this.props.changeColumnSort(col, col.desc === true ? false : col.desc === false ? undefined : true);
+ };
+
+ @action
+ changeTitleMode = () => (this._showTitleDropdown = !this._showTitleDropdown);
+
+ @computed get borderWidth() {
+ return Number(COLLECTION_BORDER_WIDTH);
+ }
+ @computed get tableColumns(): Column<Doc>[] {
+ const possibleKeys = this.props.documentKeys.filter(key => this.props.columns.findIndex(existingKey => existingKey.heading.toUpperCase() === key.toUpperCase()) === -1);
+ const columns: Column<Doc>[] = [];
+ const tableIsFocused = this.props.isFocused(this.props.Document, false);
+ const focusedRow = this._focusedCell.row;
+ const focusedCol = this._focusedCell.col;
+ const isEditable = !this.props.headerIsEditing;
+
+ columns.push({
+ expander: true,
+ Header: '',
+ width: 58,
+ Expander: rowInfo => {
+ return rowInfo.original.type !== DocumentType.COL ? null : (
+ <div className="collectionSchemaView-expander" onClick={action(() => this._openCollections[rowInfo.isExpanded ? 'delete' : 'add'](rowInfo.viewIndex))}>
+ <FontAwesomeIcon icon={rowInfo.isExpanded ? 'caret-down' : 'caret-right'} size="lg" />
+ </div>
+ );
+ },
+ });
+ columns.push(
+ ...this.props.columns.map(col => {
+ const icon: IconProp =
+ this.getColumnType(col) === ColumnType.Number
+ ? 'hashtag'
+ : this.getColumnType(col) === ColumnType.String
+ ? 'font'
+ : this.getColumnType(col) === ColumnType.Boolean
+ ? 'check-square'
+ : this.getColumnType(col) === ColumnType.Doc
+ ? 'file'
+ : this.getColumnType(col) === ColumnType.Image
+ ? 'image'
+ : this.getColumnType(col) === ColumnType.List
+ ? 'list-ul'
+ : this.getColumnType(col) === ColumnType.Date
+ ? 'calendar'
+ : 'align-justify';
+
+ const keysDropdown = (
+ <KeysDropdown
+ keyValue={col.heading}
+ possibleKeys={possibleKeys}
+ existingKeys={this.props.columns.map(c => c.heading)}
+ canAddNew={true}
+ addNew={false}
+ onSelect={this.props.changeColumns}
+ setIsEditing={this.props.setHeaderIsEditing}
+ docs={this.props.childDocs}
+ Document={this.props.Document}
+ dataDoc={this.props.dataDoc}
+ fieldKey={this.props.fieldKey}
+ ContainingCollectionDoc={this.props.ContainingCollectionDoc}
+ ContainingCollectionView={this.props.ContainingCollectionView}
+ active={this.props.active}
+ openHeader={this.props.openHeader}
+ icon={icon}
+ col={col}
+ // try commenting this out
+ width={'100%'}
+ />
+ );
+
+ const sortIcon = col.desc === undefined ? 'caret-right' : col.desc === true ? 'caret-down' : 'caret-up';
+ const header = (
+ <div className="collectionSchemaView-menuOptions-wrapper" style={{ background: col.color, padding: '2px', display: 'flex', cursor: 'default', height: '100%' }}>
+ {keysDropdown}
+ <div onClick={e => this.changeSorting(col)} style={{ width: 21, padding: 1, display: 'inline', zIndex: 1, background: 'inherit', cursor: 'pointer' }}>
+ <FontAwesomeIcon icon={sortIcon} size="lg" />
+ </div>
+ {/* {this.props.Document._chromeHidden || this.props.addDocument == returnFalse ? undefined : <div className="collectionSchemaView-addRow" onClick={this.createRow}>+ new</div>} */}
+ </div>
+ );
+
+ return {
+ Header: <MovableColumn columnRenderer={header} columnValue={col} allColumns={this.props.columns} reorderColumns={this.props.reorderColumns} ScreenToLocalTransform={this.props.ScreenToLocalTransform} />,
+ accessor: (doc: Doc) => (doc ? Field.toString(doc[col.heading] as Field) : 0),
+ id: col.heading,
+ Cell: (rowProps: CellInfo) => {
+ const rowIndex = rowProps.index;
+ const columnIndex = this.props.columns.map(c => c.heading).indexOf(rowProps.column.id!);
+ const isFocused = focusedRow === rowIndex && focusedCol === columnIndex && tableIsFocused;
+
+ const 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,
+ pinToPres: this.props.pinToPres,
+ moveDocument: this.props.moveDocument,
+ setIsEditing: this.setCellIsEditing,
+ isEditable: isEditable,
+ setPreviewDoc: this.props.setPreviewDoc,
+ setComputed: this.setComputed,
+ getField: this.getField,
+ showDoc: this.showDoc,
+ };
+
+ switch (this.getColumnType(col, rowProps.original, rowProps.column.id)) {
+ case ColumnType.Number:
+ return <CollectionSchemaNumberCell {...props} />;
+ case ColumnType.String:
+ return <CollectionSchemaStringCell {...props} />;
+ case ColumnType.Boolean:
+ return <CollectionSchemaCheckboxCell {...props} />;
+ case ColumnType.Doc:
+ return <CollectionSchemaDocCell {...props} />;
+ case ColumnType.Image:
+ return <CollectionSchemaImageCell {...props} />;
+ case ColumnType.List:
+ return <CollectionSchemaListCell {...props} />;
+ case ColumnType.Date:
+ return <CollectionSchemaDateCell {...props} />;
+ default:
+ return <CollectionSchemaCell {...props} />;
+ }
+ },
+ minWidth: 200,
+ };
+ })
+ );
+ columns.push({
+ Header: <CollectionSchemaAddColumnHeader createColumn={this.createColumn} />,
+ accessor: (doc: Doc) => 0,
+ id: 'add',
+ Cell: (rowProps: CellInfo) => {
+ const rowIndex = rowProps.index;
+ const columnIndex = this.props.columns.map(c => c.heading).indexOf(rowProps.column.id!);
+ const isFocused = focusedRow === rowIndex && focusedCol === columnIndex && tableIsFocused;
+ return (
+ <CollectionSchemaButtons
+ {...{
+ row: rowProps.index,
+ 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,
+ pinToPres: this.props.pinToPres,
+ moveDocument: this.props.moveDocument,
+ setIsEditing: this.setCellIsEditing,
+ isEditable: isEditable,
+ setPreviewDoc: this.props.setPreviewDoc,
+ setComputed: this.setComputed,
+ getField: this.getField,
+ showDoc: this.showDoc,
+ }}
+ />
+ );
+ },
+ width: 28,
+ resizable: false,
+ });
+ return columns;
+ }
+
+ constructor(props: SchemaTableProps) {
+ super(props);
+ if (this.props.Document._schemaHeaders === undefined) {
+ this.props.Document._schemaHeaders = new List<SchemaHeaderField>([
+ new SchemaHeaderField('title', '#f1efeb'),
+ new SchemaHeaderField('author', '#f1efeb'),
+ new SchemaHeaderField('*lastModified', '#f1efeb', ColumnType.Date),
+ new SchemaHeaderField('text', '#f1efeb', ColumnType.String),
+ new SchemaHeaderField('type', '#f1efeb'),
+ new SchemaHeaderField('context', '#f1efeb', ColumnType.Doc),
+ ]);
+ }
+ }
+
+ componentDidMount() {
+ document.addEventListener('keydown', this.onKeyDown);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('keydown', this.onKeyDown);
+ }
+
+ tableAddDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => {
+ const tableDoc = this.props.Document[DataSym];
+ const effectiveAcl = GetEffectiveAcl(tableDoc);
+
+ if (effectiveAcl !== AclPrivate && effectiveAcl !== AclReadonly) {
+ doc.context = this.props.Document;
+ tableDoc[this.props.fieldKey + '-lastModified'] = new DateField(new Date(Date.now()));
+ return Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before);
+ }
+ return false;
+ };
+
+ private getTrProps: ComponentPropsGetterR = (state, rowInfo) => {
+ return !rowInfo
+ ? {}
+ : {
+ ScreenToLocalTransform: this.props.ScreenToLocalTransform,
+ addDoc: this.tableAddDoc,
+ removeDoc: this.props.deleteDocument,
+ rowInfo,
+ rowFocused: !this.props.headerIsEditing && rowInfo.index === this._focusedCell.row && this.props.isFocused(this.props.Document, true),
+ textWrapRow: this.toggleTextWrapRow,
+ rowWrapped: this.textWrappedRows.findIndex(id => rowInfo.original[Id] === id) > -1,
+ dropAction: StrCast(this.props.Document.childDropAction),
+ addDocTab: this.props.addDocTab,
+ };
+ };
+
+ private getTdProps: ComponentPropsGetterR = (state, rowInfo, column, instance) => {
+ if (!rowInfo || column) return {};
+
+ const row = rowInfo.index;
+ //@ts-ignore
+ const col = this.columns.map(c => c.heading).indexOf(column!.id);
+ const isFocused = this._focusedCell.row === row && this._focusedCell.col === col && this.props.isFocused(this.props.Document, true);
+ // TODO: editing border doesn't work :(
+ return {
+ style: { border: !this.props.headerIsEditing && isFocused ? '2px solid rgb(255, 160, 160)' : '1px solid #f1efeb' },
+ };
+ };
+
+ @action setCellIsEditing = (isEditing: boolean) => (this._cellIsEditing = isEditing);
+
+ @action
+ onKeyDown = (e: KeyboardEvent): void => {
+ if (!this._cellIsEditing && !this.props.headerIsEditing && this.props.isFocused(this.props.Document, true)) {
+ // && this.props.isSelected(true)) {
+ const direction = e.key === 'Tab' ? 'tab' : e.which === 39 ? 'right' : e.which === 37 ? 'left' : e.which === 38 ? 'up' : e.which === 40 ? 'down' : '';
+ this._focusedCell = this.changeFocusedCellByDirection(direction, this._focusedCell.row, this._focusedCell.col);
+
+ if (direction) {
+ const pdoc = FieldValue(this.childDocs[this._focusedCell.row]);
+ pdoc && this.props.setPreviewDoc(pdoc);
+ e.stopPropagation();
+ }
+ } else if (e.keyCode === 27) {
+ this.props.setPreviewDoc(undefined);
+ e.stopPropagation(); // stopPropagation for left/right arrows
+ }
+ };
+
+ changeFocusedCellByDirection = (direction: string, curRow: number, curCol: number) => {
+ switch (direction) {
+ case 'tab':
+ return { row: curRow + 1 === this.childDocs.length ? 0 : curRow + 1, col: curCol + 1 === this.props.columns.length ? 0 : curCol + 1 };
+ case 'right':
+ return { row: curRow, col: curCol + 1 === this.props.columns.length ? curCol : curCol + 1 };
+ case 'left':
+ return { row: curRow, col: curCol === 0 ? curCol : curCol - 1 };
+ case 'up':
+ return { row: curRow === 0 ? curRow : curRow - 1, col: curCol };
+ case 'down':
+ return { row: curRow + 1 === this.childDocs.length ? curRow : curRow + 1, col: curCol };
+ }
+ return this._focusedCell;
+ };
+
+ @action
+ changeFocusedCellByIndex = (row: number, col: number): void => {
+ if (this._focusedCell.row !== row || this._focusedCell.col !== col) {
+ this._focusedCell = { row: row, col: col };
+ }
+ this.props.setFocused(this.props.Document);
+ };
+
+ @undoBatch
+ createRow = action(() => {
+ this.props.addDocument?.(Docs.Create.TextDocument('', { title: '', _width: 100, _height: 30 }));
+ this._focusedCell = { row: this.childDocs.length, col: this._focusedCell.col };
+ });
+
+ @undoBatch
+ @action
+ createColumn = () => {
+ const newFieldName = (index: number) => `New field${index ? ` (${index})` : ''}`;
+ for (let index = 0; index < 100; index++) {
+ if (this.props.columns.findIndex(col => col.heading === newFieldName(index)) === -1) {
+ this.props.columns.push(new SchemaHeaderField(newFieldName(index), '#f1efeb'));
+ break;
+ }
+ }
+ };
+
+ @action
+ getColumnType = (column: SchemaHeaderField, doc?: Doc, field?: string): ColumnType => {
+ if (doc && field && column.type === ColumnType.Any) {
+ const val = doc[CollectionSchemaCell.resolvedFieldKey(field, doc)];
+ if (val instanceof ImageField) return ColumnType.Image;
+ if (val instanceof Doc) return ColumnType.Doc;
+ if (val instanceof DateField) return ColumnType.Date;
+ if (val instanceof List) return ColumnType.List;
+ }
+ if (column.type && column.type !== 0) {
+ return column.type;
+ }
+ if (columnTypes.get(column.heading)) {
+ return (column.type = columnTypes.get(column.heading)!);
+ }
+ return (column.type = ColumnType.Any);
+ };
+
+ @undoBatch
+ @action
+ toggleTextwrap = async () => {
+ const textwrappedRows = Cast(this.props.Document.textwrappedSchemaRows, listSpec('string'), []);
+ if (textwrappedRows.length) {
+ this.props.Document.textwrappedSchemaRows = new List<string>([]);
+ } else {
+ const docs = DocListCast(this.props.Document[this.props.fieldKey]);
+ const allRows = docs instanceof Doc ? [docs[Id]] : docs.map(doc => doc[Id]);
+ this.props.Document.textwrappedSchemaRows = new List<string>(allRows);
+ }
+ };
+
+ @action
+ toggleTextWrapRow = (doc: Doc): void => {
+ const textWrapped = this.textWrappedRows;
+ const index = textWrapped.findIndex(id => doc[Id] === id);
+
+ index > -1 ? textWrapped.splice(index, 1) : textWrapped.push(doc[Id]);
+
+ this.textWrappedRows = textWrapped;
+ };
+
+ @computed
+ get reactTable() {
+ const children = this.childDocs;
+ const hasCollectionChild = children.reduce((found, doc) => found || doc.type === DocumentType.COL, false);
+ const expanded: { [name: string]: any } = {};
+ Array.from(this._openCollections.keys()).map(col => (expanded[col.toString()] = true));
+ const rerender = [...this.textWrappedRows]; // TODO: get component to rerender on text wrap change without needign to console.log :((((
+
+ return (
+ <ReactTable
+ style={{ position: 'relative' }}
+ data={children}
+ page={0}
+ pageSize={children.length}
+ showPagination={false}
+ columns={this.tableColumns}
+ getTrProps={this.getTrProps}
+ getTdProps={this.getTdProps}
+ sortable={false}
+ TrComponent={MovableRow}
+ sorted={this.sorted}
+ expanded={expanded}
+ resized={this.resized}
+ onResizedChange={this.props.onResizedChange}
+ // if it has a child, render another table with the children
+ SubComponent={
+ !hasCollectionChild
+ ? undefined
+ : row =>
+ row.original.type !== DocumentType.COL ? null : (
+ <div style={{ paddingLeft: 57 + 'px' }} className="reactTable-sub">
+ <SchemaTable {...this.props} Document={row.original} dataDoc={undefined} childDocs={undefined} />
+ </div>
+ )
+ }
+ />
+ );
+ }
+
+ onContextMenu = (e: React.MouseEvent): void => {
+ ContextMenu.Instance.addItem({ description: 'Toggle text wrapping', event: this.toggleTextwrap, icon: 'table' });
+ };
+
+ getField = (row: number, col?: number) => {
+ const docs = this.childDocs;
+
+ row = row % docs.length;
+ while (row < 0) row += docs.length;
+ const columns = this.props.columns;
+ const doc = docs[row];
+ if (col === undefined) {
+ return doc;
+ }
+ if (col >= 0 && col < columns.length) {
+ const column = this.props.columns[col].heading;
+ return doc[column];
+ }
+ 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;
+ }
+ }
+ }
+ }
+
+ return node;
+ }
+ return ts.visitNode(root, visit);
+ };
+ };
+
+ // const getVars = () => {
+ // return { capturedVariables: captures };
+ // };
+
+ return { transformer /*getVars*/ };
+ };
+
+ setComputed = (script: string, doc: Doc, field: string, row: number, col: number): boolean => {
+ script = `const $ = (row:number, col?:number) => {
+ const rval = (doc as any)[key][row + ${row}];
+ return col === undefined ? rval : rval[(doc as any)._schemaHeaders[col + ${col}].heading];
+ }
+ return ${script}`;
+ const compiled = CompileScript(script, { params: { this: Doc.name }, capturedVariables: { doc: this.props.Document, key: this.props.fieldKey }, typecheck: false, transformer: this.createTransformer(row, col) });
+ if (compiled.compiled) {
+ doc[field] = new ComputedField(compiled);
+ return true;
+ }
+ return false;
+ };
+
+ @action
+ showDoc = (doc: Doc | undefined, dataDoc?: Doc, screenX?: number, screenY?: number) => {
+ this._showDoc = doc;
+ if (dataDoc && screenX && screenY) {
+ this._showDocPos = this.props.ScreenToLocalTransform().transformPoint(screenX, screenY);
+ }
+ };
+
+ onOpenClick = () => {
+ this._showDoc && this.props.addDocTab(this._showDoc, 'add:right');
+ };
+
+ getPreviewTransform = (): Transform => {
+ return this.props.ScreenToLocalTransform().translate(-this.borderWidth - 4 - this.tableWidth, -this.borderWidth);
+ };
+
+ render() {
+ const preview = '';
+ return (
+ <div
+ className="collectionSchemaView-table"
+ onPointerDown={this.props.onPointerDown}
+ onClick={this.props.onClick}
+ onWheel={e => this.props.active(true) && e.stopPropagation()}
+ onDrop={e => this.props.onDrop(e, {})}
+ onContextMenu={this.onContextMenu}>
+ {this.reactTable}
+ {this.props.Document._chromeHidden || this.props.addDocument === returnFalse ? undefined : (
+ <div className="collectionSchemaView-addRow" onClick={this.createRow}>
+ + new
+ </div>
+ )}
+ {!this._showDoc ? null : (
+ <div
+ className="collectionSchemaView-documentPreview"
+ ref="overlay"
+ style={{
+ position: 'absolute',
+ width: 150,
+ height: 150,
+ background: 'dimgray',
+ display: 'block',
+ top: 0,
+ left: 0,
+ transform: `translate(${this._showDocPos[0]}px, ${this._showDocPos[1] - 180}px)`,
+ }}>
+ <DocumentView
+ Document={this._showDoc}
+ DataDoc={this._showDataDoc}
+ styleProvider={DefaultStyleProvider}
+ docViewPath={returnEmptyDoclist}
+ focus={DocUtils.DefaultFocus}
+ renderDepth={this.props.renderDepth}
+ rootSelected={returnFalse}
+ isContentActive={returnTrue}
+ isDocumentActive={returnFalse}
+ PanelWidth={() => 150}
+ PanelHeight={() => 150}
+ ScreenToLocalTransform={this.getPreviewTransform}
+ docFilters={returnEmptyFilter}
+ docRangeFilters={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ ContainingCollectionDoc={this.props.CollectionView?.props.Document}
+ ContainingCollectionView={this.props.CollectionView}
+ moveDocument={this.props.moveDocument}
+ whenChildContentsActiveChanged={emptyFunction}
+ addDocTab={this.props.addDocTab}
+ pinToPres={this.props.pinToPres}
+ bringToFront={returnFalse}></DocumentView>
+ </div>
+ )}
+ </div>
+ );
+ }
+}