diff options
Diffstat (limited to 'src')
18 files changed, 1409 insertions, 3321 deletions
diff --git a/src/.DS_Store b/src/.DS_Store Binary files differindex 717b68f3e..8f0e384aa 100644 --- a/src/.DS_Store +++ b/src/.DS_Store diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index cb429d436..0b6d4380d 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1139,8 +1139,8 @@ export namespace Docs { return Prototypes.get(DocumentType.PRESELEMENT); } - export function DataVizDocument(url: string, options?: DocumentOptions, overwriteDoc?: Doc) { - return InstanceFromProto(Prototypes.get(DocumentType.DATAVIZ), new CsvField(url), { title: 'Data Viz', ...options }, undefined, undefined, undefined, overwriteDoc); + export function DataVizDocument(url: string, options?: DocumentOptions) { + return InstanceFromProto(Prototypes.get(DocumentType.DATAVIZ), new CsvField(url), { title: 'Data Viz', ...options }); } export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) { diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 5a35fcd53..c35941ee5 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -151,10 +151,14 @@ export namespace DragManager { linkDragView: DocumentView; } export class ColumnDragData { - constructor(colKey: SchemaHeaderField) { - this.colKey = colKey; + // constructor(colKey: SchemaHeaderField) { + // this.colKey = colKey; + // } + // colKey: SchemaHeaderField; + constructor(colIndex: number) { + this.colIndex = colIndex; } - colKey: SchemaHeaderField; + colIndex: number; } // used by PDFs,Text,Image,Video,Web to conditionally (if the drop completes) create a text annotation when dragging the annotate button from the AnchorMenu when a text/region selection has been made. // this is pretty clunky and should be rethought out using linkDrag or DocumentDrag @@ -258,8 +262,8 @@ export namespace DragManager { } // drags a column from a schema view - export function StartColumnDrag(ele: HTMLElement, dragData: ColumnDragData, downX: number, downY: number, options?: DragOptions) { - StartDrag([ele], dragData, downX, downY, options); + export function StartColumnDrag(ele: HTMLElement[], dragData: ColumnDragData, downX: number, downY: number, options?: DragOptions) { + StartDrag(ele, dragData, downX, downY, options); } export function SetSnapLines(horizLines: number[], vertLines: number[]) { diff --git a/src/client/views/.DS_Store b/src/client/views/.DS_Store Binary files differindex e4ac87aad..5e39387b8 100644 --- a/src/client/views/.DS_Store +++ b/src/client/views/.DS_Store diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx deleted file mode 100644 index 18ddd881b..000000000 --- a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx +++ /dev/null @@ -1,683 +0,0 @@ -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'; -import { OpenWhere } from '../../nodes/DocumentView'; -import { PinProps } from '../../nodes/trails'; - -// 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: OpenWhere) => boolean; - pinToPres: (document: Doc, pinProps: PinProps) => 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.showDocument(this._rowDoc, { willPan: true }, () => 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, OpenWhere.addRight)} 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/collectionSchema/CollectionSchemaHeaders.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx deleted file mode 100644 index 9653f2808..000000000 --- a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx +++ /dev/null @@ -1,513 +0,0 @@ -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/collectionSchema/CollectionSchemaMovableColumn.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaMovableColumn.tsx deleted file mode 100644 index 28d2e6ab1..000000000 --- a/src/client/views/collections/collectionSchema/CollectionSchemaMovableColumn.tsx +++ /dev/null @@ -1,138 +0,0 @@ -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/collectionSchema/CollectionSchemaMovableRow.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx deleted file mode 100644 index 3cb2df7d3..000000000 --- a/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx +++ /dev/null @@ -1,152 +0,0 @@ -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 { OpenWhere } from '../../nodes/DocumentView'; -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, OpenWhere.addRight)}> - <FontAwesomeIcon icon="external-link-alt" size="sm" /> - </div> - </div> - {children} - </ReactTableDefaults.TrComponent> - </div> - </div> - ); - } -} diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss index 19401c7f0..287e2b01b 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss @@ -1,599 +1,221 @@ @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%; +.collectionSchemaView { + cursor: default; 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; -} + flex-direction: row; -.collectionSchema-col { - height: 100%; -} + .schema-table { + background-color: $white; + cursor: default; -.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; - } - } -} + .schema-column-menu, + .schema-filter-menu { + background: $light-gray; + position: absolute; + min-width: 200px; + display: flex; + flex-direction: column; + align-items: flex-start; + z-index: 1; -.collectionSchemaView-header { - height: 100%; - color: gray; - z-index: 100; - overflow-y: visible; - display: flex; - justify-content: space-between; - flex-wrap: wrap; -} + .schema-key-search-input { + width: calc(100% - 20px); + margin: 10px; + } -button.add-column { - width: 28px; -} + .schema-key-search-result { + cursor: pointer; + padding: 2px 10px; + width: 100%; -.collectionSchemaView-menuOptions-wrapper { - background: rgb(241, 239, 235); - display: flex; - cursor: default; - height: 100%; - align-content: center; - align-items: center; -} + &:hover { + background-color: $medium-gray; + } + } -.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; - } - } + .schema-key-search, + .schema-new-key-options { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + } - .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; + .schema-new-key-options { + margin: 10px; + .schema-key-warning { + color: red; + font-weight: normal; + align-self: center; + } } - } - } - .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-key-list { + width: 100%; + max-height: 300px; + overflow-y: auto; } - } - } -} -.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; -} + .schema-key-type-option { + margin: 2px 0px; -.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; - } - } -} + input { + margin-right: 5px; + } + } -.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; + .schema-key-default-val { + margin: 5px 0; + } + + .schema-column-menu-button { + cursor: pointer; + padding: 2px 5px; + background: $medium-blue; + border-radius: 9999px; + color: $white; + width: fit-content; + margin: 5px; + align-self: center; } } } - .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; - } + + .schema-preview-divider { + height: 100%; + background: black; + cursor: ew-resize; } } -.collectionSchemaView-cellContainer { - width: 100%; - height: unset; -} +.schema-header-row { + cursor: grab; + justify-content: flex-end; -.collectionSchemaView-cellContents { - width: 100%; + .row-menu { + display: flex; + justify-content: flex-end; + } } -.collectionSchemaView-cellWrapper { +.schema-column-header { + background-color: $light-gray; + font-weight: bold; display: flex; - height: 100%; - text-align: left; - padding-left: 19px; - position: relative; + flex-direction: row; + justify-content: space-between; 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; - } + padding: 0; + z-index: 1; + border: 1px solid $medium-gray; + overflow: hidden; + + .schema-column-title { + flex-grow: 2; + margin: 5px; } - &.focused { - overflow: hidden; - &.inactive { - border: none; - } + + .schema-header-menu { + margin: 5px; } - p { - width: 100%; + + .schema-column-resizer { 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; + width: 3px; + cursor: ew-resize; + + &:hover { + background-color: $light-blue; } } -} -.collectionSchemaView-cellContents-docExpander { - height: 30px; - width: 30px; - display: none; - position: absolute; - top: 0; - right: 0; - background-color: lightgray; -} + .schema-column-resizer.right { + margin-left: 5px; + align-self: flex-end; + } -.doc-drag-over { - background-color: red; + .schema-column-resizer.left { + margin-right: 5px; + align-self: flex-start; + } } -.collectionSchemaView-toolbar { - z-index: 100; +.schema-header-menu { + display: flex; + flex-direction: row; } -.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; - } +.schema-row-wrapper { + overflow: hidden; } -#preview-schema-checkbox-div { - margin-left: 20px; - font-size: 12px; +.schema-header-row { + background-color: $light-gray; } -.collectionSchemaView-table { - width: 100%; +.schema-header-row, +.schema-row { + display: flex; + flex-direction: row; height: 100%; overflow: auto; - padding: 3px; } -.rt-td.rt-expandable { - overflow: visible; - position: relative; - height: 100%; - z-index: 1; +.schema-table-cell, +.row-menu { + border: 1px solid $medium-gray; + overflow: hidden; + padding: 5px; } -.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); +.schema-row { + cursor: default; + justify-content: flex-end; + background: white; + + .row-menu { + display: flex; + flex-direction: row; + min-width: 50px; + justify-content: flex-end; } - .collectionSchemaView-table { - width: 100%; - border: solid 1px; - overflow: visible; - padding: 0px; + + .row-cells { + display: flex; + flex-direction: row; + justify-content: flex-end; } } -.collectionSchemaView-expander { - height: 100%; - min-height: 30px; - position: absolute; - color: gray; - width: 20; - height: auto; - left: 55; +.schema-row-button, +.schema-header-button { + color: $dark-gray; + margin: 3px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + svg { - position: absolute; - top: 50%; - left: 10; - transform: translate(-50%, -50%); + width: 15px; } } -.collectionSchemaView-addRow { - color: gray; - letter-spacing: 2px; - text-transform: uppercase; +.schema-sort-button { + width: 17px; + height: 17px; + border-radius: 30%; + background-color: $dark-gray; + color: white; + margin: 3px; cursor: pointer; - font-size: 10.5px; - margin-left: 50px; - margin-top: 10px; + display: flex; + align-items: center; + justify-content: center; + + svg { + width: 12px; + } } diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx index c4ee1805f..b97a2393d 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx @@ -1,108 +1,73 @@ import React = require('react'); -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, untracked } from 'mobx'; +import { action, computed, observable, ObservableMap, ObservableSet, 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 { Doc, DocListCast, Field, Opt, StrListCast } from '../../../../fields/Doc'; +import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; +import { RichTextField } from '../../../../fields/RichTextField'; 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 { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; +import { ImageField } from '../../../../fields/URLField'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTransparent, returnTrue, setupMoveUpEvents, smoothScroll, Utils } from '../../../../Utils'; +import { Docs, DocUtils } from '../../../documents/Documents'; +import { DragManager } from '../../../util/DragManager'; 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 { EditableView } from '../../EditableView'; +import { DocComponentView, DocFocusOptions, DocumentView } from '../../nodes/DocumentView'; +import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; 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 +import { SchemaColumnHeader } from './SchemaColumnHeader'; +import { SchemaRowBox } from './SchemaRowBox'; +import { DefaultStyleProvider } from '../../StyleProvider'; +import { DocumentManager } from '../../../util/DocumentManager'; +import { ScriptField } from '../../../../fields/ScriptField'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { IconButton } from 'browndash-components'; +import { KeyValueBox } from '../../nodes/KeyValueBox'; 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], -]); + +const defaultColumnKeys: string[] = ['title', 'type', 'author', 'creationDate', 'links']; @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); - } + private _ref: HTMLDivElement | null = null; + private _selectedDocSortedArray: Doc[] = []; + private _closestDropIndex: number = 0; + private _previewRef: HTMLDivElement | null = null; + private _makeNewColumn: boolean = false; + + public static _rowHeight: number = 40; + public static _minColWidth: number = 150; + public static _rowMenuWidth: number = 100; + public static _previewDividerWidth: number = 4; + + @observable _lastSelectedRow: number | undefined; + @observable _selectedDocs: ObservableSet = new ObservableSet<Doc>(); + @observable _rowEles: ObservableMap = new ObservableMap<Doc, HTMLDivElement>(); + @observable _colEles: HTMLDivElement[] = []; + @observable _isDragging: boolean = false; + @observable _displayColumnWidths: number[] | undefined; + @observable _columnMenuIndex: number | undefined; + @observable _menuOptions: string[] = []; + @observable _newFieldWarning: string = ''; + @observable _makeNewField: boolean = false; + @observable _newFieldDefault: any = 0; + @observable _newFieldType: ColumnType = ColumnType.Number; + @observable _menuValue: string = ''; + @observable _filterColumnIndex: number | undefined; + @observable _filterValue: string = ''; get documentKeys() { const docs = this.childDocs; @@ -115,532 +80,924 @@ export class CollectionSchemaView extends CollectionSubView() { //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)); + // this.columns.forEach(key => (keys[key.heading] = true)); return Array.from(Object.keys(keys)); } - @action setHeaderIsEditing = (isEditing: boolean) => (this._headerIsEditing = isEditing); + @computed get previewWidth() { + return NumCast(this.layoutDoc.schemaPreviewWidth); + } + + @computed get tableWidth() { + return this.props.PanelWidth() - this.previewWidth - (this.previewWidth === 0 ? 0 : CollectionSchemaView._previewDividerWidth); + } - @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; + @computed get columnKeys() { + return Cast(this.layoutDoc.columnKeys, listSpec('string'), defaultColumnKeys); + } + + @computed get storedColumnWidths() { + let widths = Cast( + this.layoutDoc.columnWidths, + listSpec('number'), + this.columnKeys.map(() => (this.tableWidth - CollectionSchemaView._rowMenuWidth) / this.columnKeys.length) + ); + + const totalWidth = widths.reduce((sum, width) => sum + width, 0); + if (totalWidth !== this.tableWidth - CollectionSchemaView._rowMenuWidth) { + widths = widths.map(w => { + const proportion = w / totalWidth; + return proportion * (this.tableWidth - CollectionSchemaView._rowMenuWidth); + }); } - }); - @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 + return widths; + } + + @computed get displayColumnWidths() { + return this._displayColumnWidths ?? this.storedColumnWidths; + } + + @computed get sortField() { + return StrCast(this.layoutDoc.sortField); + } + + @computed get sortDesc() { + return BoolCast(this.layoutDoc.sortDesc); + } + + rowIndex(doc: Doc) { + return this.childDocs.indexOf(doc); + } + + componentDidMount() { + this.props.setContentView?.(this as DocComponentView); + document.addEventListener('keydown', this.onKeyDown); + } + + componentWillUnmount() { + document.removeEventListener('keydown', this.onKeyDown); + } + + @action + onKeyDown = (e: KeyboardEvent) => { + if (this._selectedDocs.size > 0) { + if (e.key == 'ArrowDown') { + const lastDoc = Array.from(this._selectedDocs.values()).lastElement(); + const lastIndex = this.rowIndex(lastDoc); + if (lastIndex >= 0 && lastIndex < this.childDocs.length - 1) { + !e.shiftKey && this.clearSelection(); + const newDoc = this.childDocs[lastIndex + 1]; + this.addDocToSelection(newDoc, e.shiftKey, lastIndex + 1); + } + } + if (e.key == 'ArrowUp') { + const firstDoc = Array.from(this._selectedDocs.values())[0]; + const firstIndex = this.rowIndex(firstDoc); + if (firstIndex > 0 && firstIndex < this.childDocs.length) { + !e.shiftKey && this.clearSelection(); + const newDoc = this.childDocs[firstIndex - 1]; + this.addDocToSelection(newDoc, e.shiftKey, firstIndex - 1); + } + } } }; @undoBatch @action - setColumnSort = (columnField: SchemaHeaderField, descending: boolean | undefined) => { - const columns = this.columns; - columns.forEach(col => col.setDesc(undefined)); + setSort = (field: string | undefined, desc: boolean = false) => { + this.layoutDoc.sortField = field; + this.layoutDoc.sortDesc = desc; + + if (field === undefined) return; + + this.childDocs.sort((docA, docB) => { + const aStr = Field.toString(docA[field] as Field); + const bStr = Field.toString(docB[field] as Field); + var out = 0; + if (aStr < bStr) out = -1; + if (aStr > bStr) out = 1; + if (desc) out *= -1; + return out; + }); + }; - const index = columns.findIndex(c => c.heading === columnField.heading); - const column = columns[index]; - column.setDesc(descending); - columns[index] = column; - this.columns = columns; + addRow = (doc: Doc | Doc[]) => { + const result: boolean = this.addDocument(doc); + this.setSort(this.sortField, this.sortDesc); + return result; }; - renderTypes = (col: any) => { - if (columnTypes.get(col.heading)) return null; + @undoBatch + @action + changeColumnKey = (index: number, newKey: string, defaultVal?: any) => { + if (!this.documentKeys.includes(newKey)) { + this.addNewKey(newKey, defaultVal); + } - const type = col.type; + let currKeys = [...this.columnKeys]; + currKeys[index] = newKey; + this.layoutDoc.columnKeys = new List<string>(currKeys); + }; - const anyType = ( - <div className={'columnMenu-option' + (type === ColumnType.Any ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Any)}> - <FontAwesomeIcon icon={'align-justify'} size="sm" /> - Any - </div> - ); + @undoBatch + @action + addColumn = (key: string, defaultVal?: any) => { + if (!this.documentKeys.includes(key)) { + this.addNewKey(key, defaultVal); + } - const numType = ( - <div className={'columnMenu-option' + (type === ColumnType.Number ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Number)}> - <FontAwesomeIcon icon={'hashtag'} size="sm" /> - Number - </div> - ); + let currWidths = [...this.storedColumnWidths]; + const newColWidth = this.tableWidth / (currWidths.length + 1); + currWidths = currWidths.map(w => { + const proportion = w / (this.tableWidth - CollectionSchemaView._rowMenuWidth); + return proportion * (this.tableWidth - CollectionSchemaView._rowMenuWidth - newColWidth); + }); + currWidths.splice(0, 0, newColWidth); + this.layoutDoc.columnWidths = new List<number>(currWidths); - const textType = ( - <div className={'columnMenu-option' + (type === ColumnType.String ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.String)}> - <FontAwesomeIcon icon={'font'} size="sm" /> - Text - </div> - ); + let currKeys = [...this.columnKeys]; + currKeys.splice(0, 0, key); + this.layoutDoc.columnKeys = new List<string>(currKeys); + }; - const boolType = ( - <div className={'columnMenu-option' + (type === ColumnType.Boolean ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Boolean)}> - <FontAwesomeIcon icon={'check-square'} size="sm" /> - Checkbox - </div> - ); + @action + addNewKey = (key: string, defaultVal: any) => { + this.childDocs.forEach(doc => (doc[key] = defaultVal)); + }; - const listType = ( - <div className={'columnMenu-option' + (type === ColumnType.List ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.List)}> - <FontAwesomeIcon icon={'list-ul'} size="sm" /> - List - </div> - ); + @undoBatch + @action + removeColumn = (index: number) => { + if (this.columnKeys.length === 1) return; + let currWidths = [...this.storedColumnWidths]; + const removedColWidth = currWidths[index]; + currWidths = currWidths.map(w => { + const proportion = w / (this.tableWidth - CollectionSchemaView._rowMenuWidth - removedColWidth); + return proportion * (this.tableWidth - CollectionSchemaView._rowMenuWidth); + }); + currWidths.splice(index, 1); + this.layoutDoc.columnWidths = new List<number>(currWidths); - const docType = ( - <div className={'columnMenu-option' + (type === ColumnType.Doc ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Doc)}> - <FontAwesomeIcon icon={'file'} size="sm" /> - Document - </div> - ); + let currKeys = [...this.columnKeys]; + currKeys.splice(index, 1); + this.layoutDoc.columnKeys = new List<string>(currKeys); + }; - const imageType = ( - <div className={'columnMenu-option' + (type === ColumnType.Image ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Image)}> - <FontAwesomeIcon icon={'image'} size="sm" /> - Image - </div> - ); + @action + startResize = (e: any, index: number, left: boolean) => { + this._displayColumnWidths = this.storedColumnWidths; + setupMoveUpEvents(this, e, (e, delta) => this.resizeColumn(e, index, left), this.finishResize, emptyFunction); + }; - const dateType = ( - <div className={'columnMenu-option' + (type === ColumnType.Date ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Date)}> - <FontAwesomeIcon icon={'calendar'} size="sm" /> - Date - </div> - ); + @action + resizeColumn = (e: PointerEvent, index: number, left: boolean) => { + if (this._displayColumnWidths) { + let shrinking; + let growing; + + let change = e.movementX; + + if (left && index !== 0) { + growing = change < 0 ? index : index - 1; + shrinking = change < 0 ? index - 1 : index; + } else if (!left && index !== this.columnKeys.length - 1) { + growing = change > 0 ? index : index + 1; + shrinking = change > 0 ? index + 1 : index; + } - const allColumnTypes = ( - <div className="columnMenu-types"> - {anyType} - {numType} - {textType} - {boolType} - {listType} - {docType} - {imageType} - {dateType} - </div> - ); + if (shrinking === undefined || growing === undefined) return true; - 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; + change = Math.abs(change); + if (this._displayColumnWidths[shrinking] - change < CollectionSchemaView._minColWidth) { + change = this._displayColumnWidths[shrinking] - CollectionSchemaView._minColWidth; + } - 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> - ); - }; + this._displayColumnWidths[shrinking] -= change; + this._displayColumnWidths[growing] += change; - 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> - ); + return false; + } + return true; }; - 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> - ); + @action + finishResize = () => { + this.layoutDoc.columnWidths = new List<number>(this._displayColumnWidths); + this._displayColumnWidths = undefined; }; @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; - } - } - } - } + swapColumns = (index1: number, index2: number) => { + const tempKey = this.columnKeys[index1]; + const tempWidth = this.storedColumnWidths[index1]; + + let currKeys = this.columnKeys; + currKeys[index1] = currKeys[index2]; + currKeys[index2] = tempKey; + this.layoutDoc.columnKeys = new List<string>(currKeys); + + let currWidths = this.storedColumnWidths; + currWidths[index1] = currWidths[index2]; + currWidths[index2] = tempWidth; + this.layoutDoc.columnWidths = new List<number>(currWidths); }; @action - openHeader = (col: any, screenx: number, screeny: number) => { - this._col = col; - this._headerOpen = true; - this._pointerX = screenx; - this._pointerY = screeny; + dragColumn = (e: PointerEvent, index: number) => { + const dragData = new DragManager.ColumnDragData(index); + const dragEles = [this._colEles[index]]; + this.childDocs.forEach(doc => { + dragEles.push(this._rowEles.get(doc).children[1].children[index]); + }); + DragManager.StartColumnDrag(dragEles, dragData, e.x, e.y); + + return true; }; @action - closeHeader = () => { - this._headerOpen = false; + addRowRef = (doc: Doc, ref: HTMLDivElement) => { + this._rowEles.set(doc, ref); }; - @undoBatch @action - deleteColumn = (key: string) => { - const columns = this.columns; - if (columns === undefined) { - this.columns = new List<SchemaHeaderField>([]); + setColRef = (index: number, ref: HTMLDivElement) => { + if (this._colEles.length <= index) { + this._colEles.push(ref); } else { - const index = columns.map(c => c.heading).indexOf(key); - if (index > -1) { - columns.splice(index, 1); - this.columns = columns; - } + this._colEles[index] = ref; } - this.closeHeader(); }; - getPreviewTransform = (): Transform => { - return this.props.ScreenToLocalTransform().translate(-this.borderWidth - NumCast(COLLECTION_BORDER_WIDTH) - this.tableWidth, -this.borderWidth); + @action + addDocToSelection = (doc: Doc, extendSelection: boolean, index: number) => { + this._selectedDocs.add(doc); + const rowDocView = DocumentManager.Instance.getDocumentView(doc); + if (rowDocView) SelectionManager.SelectView(rowDocView, extendSelection); + this._lastSelectedRow = index; }; @action - onHeaderClick = (e: React.PointerEvent) => { - e.stopPropagation(); + removeDocFromSelection = (doc: Doc) => { + if (this._selectedDocs.has(doc)) this._selectedDocs.delete(doc); + const rowDocView = DocumentManager.Instance.getDocumentView(doc); + if (rowDocView) SelectionManager.DeselectView(rowDocView); + if (this._selectedDocs.size === 0) { + this._lastSelectedRow = undefined; + } }; @action - onWheel(e: React.WheelEvent) { - const scale = this.props.ScreenToLocalTransform().Scale; - this.props.isContentActive(true) && e.stopPropagation(); - } + clearSelection = () => { + this._selectedDocs.clear(); + SelectionManager.DeselectAll(); + this._lastSelectedRow = undefined; + }; - @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> - ); - } + rowOnClickScript = ScriptField.MakeFunction('scriptContext.selectRow(self, shiftKey, ctrlKey || metaKey)', { scriptContext: 'any', shiftKey: 'boolean', ctrlKey: 'boolean', metaKey: 'boolean' })!; - private createTarget = (ele: HTMLDivElement) => { - this._previewCont = ele; - super.CreateDropTarget(ele); + @action + selectRow = (doc: Doc, shift: boolean, ctrl: boolean) => { + const index = this.childDocs.indexOf(doc); + if (index < 0) return; + if (shift && this._lastSelectedRow !== undefined) { + const startRow = Math.min(this._lastSelectedRow, index); + const endRow = Math.max(this._lastSelectedRow, index); + for (let i = startRow; i <= endRow; i++) { + const currDoc = this.childDocs[i]; + if (!this._selectedDocs.has(currDoc)) this.addDocToSelection(currDoc, true, i); + } + this._lastSelectedRow = index; + } else if (ctrl) { + if (!this._selectedDocs.has(doc)) { + this.addDocToSelection(doc, true, index); + } else { + this.removeDocFromSelection(doc); + } + } else { + if (!this._selectedDocs.has(doc)) { + this.clearSelection(); + this.addDocToSelection(doc, false, index); + } + } + }; + + @action + sortedSelectedDocs = (): Doc[] => { + return this.childDocs.filter(doc => this._selectedDocs.has(doc)); }; - isFocused = (doc: Doc, outsideReaction: boolean): boolean => this.props.isSelected(outsideReaction) && doc === this._focusedTable; + setDropIndex = (index: number) => { + this._closestDropIndex = index; + }; - @action setFocused = (doc: Doc) => (this._focusedTable = doc); + @action + onInternalDrop = (e: Event, de: DragManager.DropEvent) => { + if (de.complete.columnDragData) { + e.stopPropagation(); + const mouseX = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y)[0]; + let i = de.complete.columnDragData.colIndex; + this.displayColumnWidths.reduce((total, curr, index) => { + if (total <= mouseX && total + curr >= mouseX) { + i = index; + } + return total + curr; + }, CollectionSchemaView._rowMenuWidth); + this.swapColumns(de.complete.columnDragData.colIndex, i); + return true; + } + if (super.onInternalDrop(e, de)) { + this._isDragging = false; + const pushedDocs: Doc[] = this.childDocs.filter((doc: Doc, index: number) => index >= this._closestDropIndex && !this._selectedDocs.has(doc)); + this.props.removeDocument?.(pushedDocs); + this.props.removeDocument?.(this._selectedDocSortedArray); + this.addDocument(this._selectedDocSortedArray); + this.addDocument(pushedDocs); + this.setSort(undefined); + this.clearSelection(); + return true; + } + return false; + }; - @action setPreviewDoc = (doc: Opt<Doc>) => { - SelectionManager.SelectSchemaViewDoc(doc); - this._previewDoc = doc; + @action + onExternalDrop = async (e: React.DragEvent): Promise<void> => { + super.onExternalDrop( + e, + {}, + undoBatch( + action(docus => { + this._isDragging = false; + docus.map((doc: Doc) => { + this.addDocument(doc); + }); + }) + ) + ); + this.setSort(undefined); }; - //toggles preview side-panel of schema @action - toggleExpander = () => { - this.props.Document.schemaPreviewWidth = this.previewWidth() === 0 ? Math.min(this.tableWidth / 3, 200) : 0; + startDrag = (e: PointerEvent, doc: Doc, index: number) => { + if (!this._selectedDocs.has(doc)) { + this.clearSelection(); + this.addDocToSelection(doc, false, index); + } + this._isDragging = true; + this._selectedDocSortedArray = this.sortedSelectedDocs(); + const dragData = new DragManager.DocumentDragData(this._selectedDocSortedArray, 'move'); + dragData.moveDocument = this.props.moveDocument; + const dragItem: HTMLElement[] = Array.from(this._selectedDocs.values()).map((doc: Doc) => this._rowEles.get(doc)); + + DragManager.StartDocumentDrag( + dragItem.map(ele => ele), + dragData, + e.clientX, + e.clientY, + undefined + ); + return true; }; onDividerDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, this.onDividerMove, emptyFunction, this.toggleExpander); + setupMoveUpEvents(this, e, this.onDividerMove, emptyFunction, emptyFunction); }; + @action onDividerMove = (e: PointerEvent, down: number[], delta: number[]) => { - const nativeWidth = this._previewCont!.getBoundingClientRect(); + const nativeWidth = this._previewRef!.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; + this.layoutDoc.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); + @action + addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => { + if (!value && !forceEmptyNote) return false; + const newDoc = Docs.Create.TextDocument(value, { title: value, _autoHeight: true }); + FormattedTextBox.SelectOnLoad = newDoc[Id]; + FormattedTextBox.SelectOnLoadChar = forceEmptyNote ? '' : ' '; + return this.addRow(newDoc) || false; + }; + + menuCallback = (x: number, y: number) => { + ContextMenu.Instance.clearItems(); + const layoutItems: ContextMenuProps[] = []; + const docItems: ContextMenuProps[] = []; + const dataDoc = this.props.DataDoc || this.props.Document; + + DocUtils.addDocumentCreatorMenuItems( + doc => { + FormattedTextBox.SelectOnLoad = StrCast(doc[Id]); + return this.addRow(doc); + }, + this.addRow, + x, + y, + true + ); + + Array.from(Object.keys(Doc.GetProto(dataDoc))) + .filter(fieldKey => dataDoc[fieldKey] instanceof RichTextField || dataDoc[fieldKey] instanceof ImageField || typeof dataDoc[fieldKey] === 'string') + .map(fieldKey => + docItems.push({ + description: ':' + fieldKey, + event: () => { + const created = DocUtils.DocumentFromField(dataDoc, fieldKey, Doc.GetProto(this.props.Document)); + if (created) { + if (this.props.Document.isTemplateDoc) { + Doc.MakeMetadataFieldTemplate(created, this.props.Document); + } + return this.addRow(created); + } + }, + icon: 'compress-arrows-alt', + }) + ); + Array.from(Object.keys(Doc.GetProto(dataDoc))) + .filter(fieldKey => DocListCast(dataDoc[fieldKey]).length) + .map(fieldKey => + docItems.push({ + description: ':' + fieldKey, + event: () => { + const created = Docs.Create.CarouselDocument([], { _width: 400, _height: 200, title: fieldKey }); + if (created) { + const container = this.props.Document.resolvedDataDoc ? Doc.GetProto(this.props.Document) : this.props.Document; + if (container.isTemplateDoc) { + Doc.MakeMetadataFieldTemplate(created, container); + return Doc.AddDocToList(container, Doc.LayoutFieldKey(container), created); + } + return this.addRow(created) || false; + } + }, + icon: 'compress-arrows-alt', + }) + ); + !Doc.noviceMode && ContextMenu.Instance.addItem({ description: 'Doc Fields ...', subitems: docItems, icon: 'eye' }); + !Doc.noviceMode && ContextMenu.Instance.addItem({ description: 'Containers ...', subitems: layoutItems, icon: 'eye' }); + ContextMenu.Instance.setDefaultItem('::', (name: string): void => { + Doc.GetProto(this.props.Document)[name] = ''; + const created = Docs.Create.TextDocument('', { title: name, _autoHeight: true }); + if (created) { + if (this.props.Document.isTemplateDoc) { + Doc.MakeMetadataFieldTemplate(created, this.props.Document); + } + this.addRow(created); + } + }); + ContextMenu.Instance.displayMenu(x, y, undefined, true); + }; + + focusDocument = (doc: Doc, options: DocFocusOptions) => { + Doc.BrushDoc(doc); + + const found = this._mainCont && Array.from(this._mainCont.getElementsByClassName('documentView-node')).find((node: any) => node.id === doc[Id]); + if (found) { + const top = found.getBoundingClientRect().top; + const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top); + if (Math.floor(localTop[1]) !== 0) { + let focusSpeed = options.zoomTime ?? 500; + smoothScroll(focusSpeed, this._mainCont!, localTop[1] + this._mainCont!.scrollTop, options.easeFunc); + return focusSpeed; + } } + return undefined; }; - @computed - get previewDocument(): Doc | undefined { - return this._previewDoc; + isChildContentActive = () => + this.props.isDocumentActive?.() && (this.props.childDocumentsActive?.() || BoolCast(this.rootDoc.childDocumentsActive)) ? true : this.props.childDocumentsActive?.() === false || this.rootDoc.childDocumentsActive === false ? false : undefined; + + @computed get fieldDefaultInput() { + switch (this._newFieldType) { + case ColumnType.Number: + return <input type="number" name="" id="" value={this._newFieldDefault ?? 0} onPointerDown={e => e.stopPropagation()} onChange={action(e => (this._newFieldDefault = e.target.value))} />; + case ColumnType.Boolean: + return ( + <> + <input type="checkbox" name="" id="" value={this._newFieldDefault} onPointerDown={e => e.stopPropagation()} onChange={action(e => (this._newFieldDefault = e.target.checked))} /> + {this._newFieldDefault ? 'true' : 'false'} + </> + ); + case ColumnType.String: + return <input type="text" name="" id="" value={this._newFieldDefault ?? ''} onPointerDown={e => e.stopPropagation()} onChange={action(e => (this._newFieldDefault = e.target.value))} />; + } } - @computed - get dividerDragger() { - return this.previewWidth() === 0 ? null : ( - <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown}> - <div className="collectionSchemaView-dividerDragger" /> - </div> - ); - } + onSearchKeyDown = (e: React.KeyboardEvent) => { + switch (e.key) { + case 'Enter': + this._menuOptions.length > 0 && this._menuValue.length > 0 ? this.setKey(this._menuOptions[0]) : action(() => (this._makeNewField = true))(); + break; + case 'Escape': + this.closeColumnMenu(); + break; + } + }; + + @action + setKey = (key: string, defaultVal?: any) => { + if (this._makeNewColumn) { + this.addColumn(key, defaultVal); + } else { + this.changeColumnKey(this._columnMenuIndex!, key, defaultVal); + } + this.closeColumnMenu(); + }; + + setColumnValues = (key: string, value: string) => { + let success: boolean = true; + this.childDocs.forEach(doc => success && KeyValueBox.SetField(doc, key, value)); + return success; + }; + + @action + openColumnMenu = (index: number, newCol: boolean) => { + this._makeNewColumn = false; + this._columnMenuIndex = index; + this._menuValue = ''; + this._menuOptions = this.documentKeys; + this._makeNewField = false; + this._newFieldWarning = ''; + this._makeNewField = false; + this._makeNewColumn = newCol; + }; + + @action + closeColumnMenu = () => { + this._columnMenuIndex = undefined; + }; + + @action + openFilterMenu = (index: number) => { + this._filterColumnIndex = index; + this._filterValue = this.getFieldFilters(this.columnKeys[this._filterColumnIndex!]).map(filter => filter.split(':')[1])[0]; + }; + + @action + closeFilterMenu = () => { + this._filterColumnIndex = undefined; + }; + + openContextMenu = (x: number, y: number, index: number) => { + this.closeColumnMenu(); + this.closeFilterMenu(); + ContextMenu.Instance.clearItems(); + ContextMenu.Instance.addItem({ + description: 'Change field', + event: () => { + this.openColumnMenu(index, false); + }, + icon: 'pencil-alt', + }); + ContextMenu.Instance.addItem({ + description: 'Filter field', + event: () => { + this.openFilterMenu(index); + }, + icon: 'filter', + }); + ContextMenu.Instance.addItem({ + description: 'Delete column', + event: () => { + this.removeColumn(index); + }, + icon: 'trash', + }); + ContextMenu.Instance.displayMenu(x, y, undefined, false); + }; + + @action + updateKeySearch = (e: React.ChangeEvent<HTMLInputElement>) => { + this._menuValue = e.target.value; + this._menuOptions = this.documentKeys.filter(value => value.toLowerCase().includes(this._menuValue.toLowerCase())); + }; + + getFieldFilters = (field: string) => { + return StrListCast(this.Document._docFilters).filter(filter => filter.split(':')[0] == field); + }; + + removeFieldFilters = (field: string) => { + this.getFieldFilters(field).forEach(filter => { + Doc.setDocFilter(this.Document, field, filter.split(':')[1], 'remove'); + }); + }; + + onFilterKeyDown = (e: React.KeyboardEvent) => { + switch (e.key) { + case 'Enter': + if (this._filterValue !== '') { + Doc.setDocFilter(this.Document, this.columnKeys[this._filterColumnIndex!], this._filterValue, 'check', false, undefined, false); + } else { + this.removeFieldFilters(this.columnKeys[this._filterColumnIndex!]); + } + this.closeFilterMenu(); + break; + case 'Escape': + this.closeFilterMenu(); + break; + } + }; + + @action + updateFilterSearch = (e: React.ChangeEvent<HTMLInputElement>) => { + this._filterValue = e.target.value; + }; - @computed - get previewPanel() { + @computed get newFieldMenu() { 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 className="schema-new-key-options"> + <div className="schema-key-type-option"> + <input + type="radio" + name="newFieldType" + id="" + checked={this._newFieldType == ColumnType.Number} + onChange={action(() => { + this._newFieldType = ColumnType.Number; + this._newFieldDefault = 0; + })} /> - )} + number + </div> + <div className="schema-key-type-option"> + <input + type="radio" + name="newFieldType" + id="" + checked={this._newFieldType == ColumnType.Boolean} + onChange={action(() => { + this._newFieldType = ColumnType.Boolean; + this._newFieldDefault = false; + })} + /> + boolean + </div> + <div className="schema-key-type-option"> + <input + type="radio" + name="newFieldType" + id="" + checked={this._newFieldType == ColumnType.String} + onChange={action(() => { + this._newFieldType = ColumnType.String; + this._newFieldDefault = ''; + })} + /> + string + </div> + <div className="schema-key-default-val">value: {this.fieldDefaultInput}</div> + <div className="schema-key-warning">{this._newFieldWarning}</div> + <div + className="schema-column-menu-button" + onPointerDown={action(e => { + if (this.documentKeys.includes(this._menuValue)) { + this._newFieldWarning = 'Field already exists'; + } else if (this._menuValue.length === 0) { + this._newFieldWarning = 'Field cannot be an empty string'; + } else { + this.setKey(this._menuValue, this._newFieldDefault); + } + })}> + done + </div> </div> ); } - @computed - get schemaTable() { + @computed get keysDropdown() { 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} - /> + <div className="schema-key-search"> + <div + className="schema-column-menu-button" + onPointerDown={action(e => { + e.stopPropagation(); + this._makeNewField = true; + })}> + + new field + </div> + <div className="schema-key-list"> + {this._menuOptions.map(key => ( + <div + className="schema-key-search-result" + onPointerDown={e => { + e.stopPropagation(); + this.setKey(key); + }}> + {key} + </div> + ))} + </div> + </div> ); } - @computed - public get schemaToolbar() { + @computed get renderColumnMenu() { + const x = this._columnMenuIndex! == -1 ? 0 : this.displayColumnWidths.reduce((total, curr, index) => total + (index < this._columnMenuIndex! ? curr : 0), CollectionSchemaView._rowMenuWidth); 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 className="schema-column-menu" style={{ left: x, minWidth: CollectionSchemaView._minColWidth }}> + <input className="schema-key-search-input" type="text" value={this._menuValue} onKeyDown={this.onSearchKeyDown} onChange={this.updateKeySearch} onPointerDown={e => e.stopPropagation()} /> + {this._makeNewField ? this.newFieldMenu : this.keysDropdown} + <div + className="schema-column-menu-button" + onPointerDown={action(e => { + e.stopPropagation(); + this.closeColumnMenu(); + })}> + cancel </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(); - } - }; + @computed get renderFilterOptions() { + const keyOptions: string[] = []; + const columnKey = this.columnKeys[this._filterColumnIndex!]; + this.childDocs.forEach(doc => { + const key = StrCast(doc[columnKey]); + if (keyOptions.includes(key) === false && (key.includes(this._filterValue) || !this._filterValue) && key !== '') { + keyOptions.push(key); + } + }); - @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(); + const filters = StrListCast(this.Document._docFilters); + for (let i = 0; i < (filters?.length ?? 0) - 1; i++) { + if (filters[i] === columnKey && keyOptions.includes(filters[i].split(':')[1]) === false) { + keyOptions.push(filters[i + 1]); + } } - 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; + 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="schema-filter-option"> + <input + type="checkbox" + onPointerDown={e => e.stopPropagation()} + onClick={e => e.stopPropagation()} + onChange={action(e => { + if (e.target.checked) { + Doc.setDocFilter(this.props.Document, columnKey, key, 'check'); + } else { + Doc.setDocFilter(this.props.Document, columnKey, key, 'remove'); + } + })} + checked={bool} + /> + <span style={{ paddingLeft: 4 }}>{key}</span> + </div> + ); }); - 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(); + return options; + } - 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]; + @computed get renderFilterMenu() { + const x = this.displayColumnWidths.reduce((total, curr, index) => total + (index < this._filterColumnIndex! ? curr : 0), CollectionSchemaView._rowMenuWidth); + return ( + <div className="schema-filter-menu" style={{ left: x, minWidth: CollectionSchemaView._minColWidth }}> + <input className="schema-filter-input" type="text" value={this._filterValue} onKeyDown={this.onFilterKeyDown} onChange={this.updateFilterSearch} onPointerDown={e => e.stopPropagation()} /> + {this.renderFilterOptions} + <div + className="schema-column-menu-button" + onPointerDown={action(e => { + e.stopPropagation(); + this.closeFilterMenu(); })}> - {({ measureRef }) => <div ref={measureRef}> {menuContent} </div>} - </Measure> + done + </div> </div> ); + } + + render() { 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', - }}> + className="collectionSchemaView" + ref={(ele: HTMLDivElement | null) => { + this._ref = ele; + this.createDashEventsTarget(ele); + }} + onDrop={this.onExternalDrop.bind(this)}> <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} + className="schema-table" + onPointerDown={action(() => { + this.clearSelection(); + })}> + <div className="schema-header-row" style={{ height: CollectionSchemaView._rowHeight }}> + <div className="row-menu" style={{ width: CollectionSchemaView._rowMenuWidth }}> + <div + className="schema-header-button" + onPointerDown={e => { + this._columnMenuIndex && this._columnMenuIndex === -1 ? this.closeColumnMenu() : this.openColumnMenu(-1, true); + }}> + <FontAwesomeIcon icon="plus" /> + </div> + </div> + {this.columnKeys.map((key, index) => { + return ( + <SchemaColumnHeader + key={index} + columnIndex={index} + columnKeys={this.columnKeys} + columnWidths={this.displayColumnWidths} + sortField={this.sortField} + sortDesc={this.sortDesc} + setSort={this.setSort} + removeColumn={this.removeColumn} + resizeColumn={this.startResize} + openContextMenu={this.openContextMenu} + dragColumn={this.dragColumn} + setColRef={this.setColRef} + /> + ); + })} + </div> + {this._columnMenuIndex !== undefined && this.renderColumnMenu} + {this._filterColumnIndex !== undefined && this.renderFilterMenu} + <div className="schema-table-content" onPointerDown={e => e.stopPropagation()}> + {this.childDocs.map((doc: Doc, index: number) => { + const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField && !doc.PARAMS ? undefined : this.props.DataDoc; + let dref: Opt<DocumentView>; + return ( + <div className="schema-row-wrapper" style={{ maxHeight: CollectionSchemaView._rowHeight }}> + <DocumentView + {...this.props} + ref={r => (dref = r || undefined)} + LayoutTemplate={this.props.childLayoutTemplate} + LayoutTemplateString={SchemaRowBox.LayoutString(this.props.fieldKey)} + Document={doc} + DataDoc={dataDoc} + ContainingCollectionView={this.props.CollectionView} + ContainingCollectionDoc={this.Document} + PanelWidth={() => this.tableWidth} + PanelHeight={() => CollectionSchemaView._rowHeight} + styleProvider={DefaultStyleProvider} + focus={this.focusDocument} + docFilters={this.childDocFilters} + docRangeFilters={this.childDocRangeFilters} + searchFilterDocs={this.searchFilterDocs} + rootSelected={this.rootSelected} + ScreenToLocalTransform={Transform.Identity} + bringToFront={emptyFunction} + isContentActive={this.isChildContentActive} + hideDecorations={true} + hideTitle={true} + hideDocumentButtonBar={true} + hideLinkAnchors={true} + fitWidth={returnTrue} + onClick={() => this.rowOnClickScript} + scriptContext={this} + /> + </div> + ); + })} + </div> + <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} /> </div> - {this.dividerDragger} - {!this.previewWidth() ? null : this.previewPanel} - {this._headerOpen && this.props.isContentActive() ? menu : null} + {this.previewWidth > 0 && <div className="schema-preview-divider" style={{ width: CollectionSchemaView._previewDividerWidth }} onPointerDown={this.onDividerDown}></div>} + {this.previewWidth > 0 && ( + <div style={{ width: `${this.previewWidth}px` }} ref={ref => (this._previewRef = ref)}> + {this._lastSelectedRow !== undefined && ( + <DocumentView + Document={this.childDocs[this._lastSelectedRow]} + DataDoc={undefined} + fitContentsToBox={returnTrue} + dontCenter={'y'} + focus={DocUtils.DefaultFocus} + renderDepth={this.props.renderDepth} + rootSelected={this.rootSelected} + PanelWidth={() => this.previewWidth} + PanelHeight={this.props.PanelHeight} + isContentActive={returnTrue} + isDocumentActive={returnFalse} + ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-this.tableWidth, 0)} + 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.addRow} + removeDocument={this.props.removeDocument} + whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} + addDocTab={this.props.addDocTab} + pinToPres={this.props.pinToPres} + bringToFront={returnFalse} + /> + )} + </div> + )} </div> ); } diff --git a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx new file mode 100644 index 000000000..e648356f4 --- /dev/null +++ b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx @@ -0,0 +1,81 @@ +import React = require('react'); +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import { emptyFunction, setupMoveUpEvents } from '../../../../Utils'; +import './CollectionSchemaView.scss'; +import { ColumnType } from './CollectionSchemaView'; +import { IconButton } from 'browndash-components'; +import { Colors } from '../../global/globalEnums'; +import { ContextMenu } from '../../ContextMenu'; +import { Doc, DocListCast } from '../../../../fields/Doc'; +import { Id } from '../../../../fields/FieldSymbols'; +import { RichTextField } from '../../../../fields/RichTextField'; +import { StrCast } from '../../../../fields/Types'; +import { ImageField } from '../../../../fields/URLField'; +import { DocUtils, Docs } from '../../../documents/Documents'; +import { ContextMenuProps } from '../../ContextMenuItem'; +import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; + +export interface SchemaColumnHeaderProps { + columnKeys: string[]; + columnWidths: number[]; + columnIndex: number; + sortField: string; + sortDesc: boolean; + setSort: (field: string, desc: boolean) => void; + removeColumn: (index: number) => void; + resizeColumn: (e: any, index: number, left: boolean) => void; + dragColumn: (e: any, index: number) => boolean; + openContextMenu: (x: number, y: number, index: number) => void; + setColRef: (index: number, ref: HTMLDivElement) => void; +} + +@observer +export class SchemaColumnHeader extends React.Component<SchemaColumnHeaderProps> { + @computed get fieldKey() { + return this.props.columnKeys[this.props.columnIndex]; + } + + @action + sortClicked = (e: React.PointerEvent) => { + e.stopPropagation(); + e.preventDefault(); + if (this.props.sortField == this.fieldKey) { + this.props.setSort(this.fieldKey, !this.props.sortDesc); + } else { + this.props.setSort(this.fieldKey, false); + } + }; + + @action + onPointerDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, e => this.props.dragColumn(e, this.props.columnIndex), emptyFunction, emptyFunction); + }; + + render() { + return ( + <div + className="schema-column-header" + style={{ width: this.props.columnWidths[this.props.columnIndex] }} + onPointerDown={this.onPointerDown} + ref={(col: HTMLDivElement | null) => { + col && this.props.setColRef(this.props.columnIndex, col); + }}> + <div className="schema-column-resizer left" onPointerDown={e => this.props.resizeColumn(e, this.props.columnIndex, true)}></div> + <div className="schema-column-title">{this.fieldKey}</div> + + <div className="schema-header-menu"> + <div className="schema-header-button" onPointerDown={e => this.props.openContextMenu(e.clientX, e.clientY, this.props.columnIndex)}> + <FontAwesomeIcon icon="ellipsis-h" /> + </div> + <div className="schema-sort-button" onPointerDown={this.sortClicked} style={this.props.sortField == this.fieldKey ? { backgroundColor: Colors.MEDIUM_BLUE } : {}}> + <FontAwesomeIcon icon="caret-right" style={this.props.sortField == this.fieldKey ? { transform: `rotate(${this.props.sortDesc ? '270deg' : '90deg'})` } : {}} /> + </div> + </div> + + <div className="schema-column-resizer right" onPointerDown={e => this.props.resizeColumn(e, this.props.columnIndex, false)}></div> + </div> + ); + } +} diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx new file mode 100644 index 000000000..37999484d --- /dev/null +++ b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx @@ -0,0 +1,136 @@ +import React = require('react'); +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, ObservableSet } from 'mobx'; +import { observer } from 'mobx-react'; +import { Doc } from '../../../../fields/Doc'; +import { undoBatch } from '../../../util/UndoManager'; +import { ViewBoxBaseComponent } from '../../DocComponent'; +import { OpenWhere } from '../../nodes/DocumentView'; +import { FieldView, FieldViewProps } from '../../nodes/FieldView'; +import { CollectionSchemaView } from './CollectionSchemaView'; +import './CollectionSchemaView.scss'; +import { SchemaTableCell } from './SchemaTableCell'; +import { Colors } from '../../global/globalEnums'; +import { DocCast, StrCast } from '../../../../fields/Types'; +import { setupMoveUpEvents, emptyFunction } from '../../../../Utils'; +import { DragManager } from '../../../util/DragManager'; + +@observer +export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() { + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(SchemaRowBox, fieldKey); + } + + private _ref: HTMLDivElement | null = null; + + bounds = () => this._ref?.getBoundingClientRect(); + + @computed get schemaView() { + const vpath = this.props.docViewPath(); + return vpath.length > 1 ? (vpath[vpath.length - 2].ComponentView as CollectionSchemaView) : undefined; + } + + @computed get schemaDoc() { + return this.props.ContainingCollectionDoc!; + } + + @computed get rowIndex() { + return this.schemaView?.rowIndex(this.rootDoc) ?? -1; + } + + @action + onRowPointerDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, e => this.schemaView?.startDrag(e, this.rootDoc, this.rowIndex) ?? true, emptyFunction, emptyFunction, false); + }; + + onPointerEnter = (e: any) => { + if (!this.schemaView?._isDragging) return; + document.removeEventListener('pointermove', this.onPointerMove); + document.addEventListener('pointermove', this.onPointerMove); + }; + + onPointerMove = (e: any) => { + if (!this.schemaView?._isDragging) return; + let dragIsRow: boolean = true; + DragManager.docsBeingDragged.forEach(doc => { + dragIsRow = this.schemaView?._selectedDocs.has(doc) ?? false; + }); + if (this._ref && dragIsRow) { + const rect = this._ref.getBoundingClientRect(); + const y = e.clientY - rect.top; //y position within the element. + const height = this._ref.clientHeight; + const halfLine = height / 2; + if (y <= halfLine) { + this._ref.style.borderTop = `solid 2px ${Colors.MEDIUM_BLUE}`; + this._ref.style.borderBottom = '0px'; + this.schemaView?.setDropIndex(this.rowIndex); + } else if (y > halfLine) { + this._ref.style.borderTop = '0px'; + this._ref.style.borderBottom = `solid 2px ${Colors.MEDIUM_BLUE}`; + this.schemaView?.setDropIndex(this.rowIndex + 1); + } + } + }; + + onPointerLeave = (e: any) => { + if (this._ref) { + this._ref.style.borderTop = '0px'; + this._ref.style.borderBottom = '0px'; + } + document.removeEventListener('pointermove', this.onPointerMove); + }; + + render() { + return ( + <div + className="schema-row" + style={ + this.props.isSelected() + ? { height: CollectionSchemaView._rowHeight, backgroundColor: Colors.LIGHT_BLUE, pointerEvents: this.schemaView?.props.isContentActive() ? 'all' : undefined /*, opacity: this.props.dragging ? 0.5 : 1 */ } + : { height: CollectionSchemaView._rowHeight, pointerEvents: this.schemaView?.props.isContentActive() ? 'all' : undefined } + } + onPointerDown={this.onRowPointerDown} + onPointerEnter={this.onPointerEnter} + onPointerLeave={this.onPointerLeave} + ref={(row: HTMLDivElement | null) => { + row && this.schemaView?.addRowRef?.(this.rootDoc, row); + this._ref = row; + }}> + <div + className="row-menu" + style={{ + width: CollectionSchemaView._rowMenuWidth, + }}> + <div + className="schema-row-button" + onPointerDown={undoBatch(e => { + e.stopPropagation(); + this.props.removeDocument?.(this.rootDoc); + })}> + <FontAwesomeIcon icon="times" /> + </div> + <div + className="schema-row-button" + onPointerDown={e => { + e.stopPropagation(); + this.props.addDocTab(this.rootDoc, OpenWhere.addRight); + }}> + <FontAwesomeIcon icon="external-link-alt" /> + </div> + </div> + <div className="row-cells"> + {this.schemaView?.columnKeys?.map((key, index) => ( + <SchemaTableCell + key={key} + Document={this.rootDoc} + fieldKey={key} + columnWidth={this.schemaView?.displayColumnWidths[index] ?? CollectionSchemaView._minColWidth} + isRowActive={this.props.isContentActive} + setColumnValues={(field, value) => this.schemaView?.setColumnValues(field, value) ?? false} + /> + ))} + </div> + </div> + ); + } +} diff --git a/src/client/views/collections/collectionSchema/SchemaTable.tsx b/src/client/views/collections/collectionSchema/SchemaTable.tsx deleted file mode 100644 index 16910cc83..000000000 --- a/src/client/views/collections/collectionSchema/SchemaTable.tsx +++ /dev/null @@ -1,694 +0,0 @@ -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, OpenWhere } from '../../nodes/DocumentView'; -import { PinProps } from '../../nodes/trails'; -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 './CollectionSchemaMovableColumn'; -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: OpenWhere) => boolean; - pinToPres: (document: Doc, pinProps: PinProps) => 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, OpenWhere.addRight); - }; - - 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> - ); - } -} diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx new file mode 100644 index 000000000..13e45963e --- /dev/null +++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx @@ -0,0 +1,69 @@ +import React = require('react'); +import { observer } from 'mobx-react'; +import { Doc, Field } from '../../../../fields/Doc'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero } from '../../../../Utils'; +import { Transform } from '../../../util/Transform'; +import { EditableView } from '../../EditableView'; +import { FieldView, FieldViewProps } from '../../nodes/FieldView'; +import { KeyValueBox } from '../../nodes/KeyValueBox'; +import { DefaultStyleProvider } from '../../StyleProvider'; +import { CollectionSchemaView } from './CollectionSchemaView'; +import './CollectionSchemaView.scss'; + +export interface SchemaTableCellProps { + Document: Doc; + fieldKey: string; + columnWidth: number; + isRowActive: () => boolean | undefined; + setColumnValues: (field: string, value: string) => boolean; +} + +@observer +export class SchemaTableCell extends React.Component<SchemaTableCellProps> { + render() { + const props: FieldViewProps = { + Document: this.props.Document, + docFilters: returnEmptyFilter, + docRangeFilters: returnEmptyFilter, + searchFilterDocs: returnEmptyDoclist, + styleProvider: DefaultStyleProvider, + docViewPath: returnEmptyDoclist, + ContainingCollectionView: undefined, + ContainingCollectionDoc: undefined, + fieldKey: this.props.fieldKey, + rootSelected: returnFalse, + isSelected: returnFalse, + setHeight: returnFalse, + select: emptyFunction, + dropAction: 'alias', + bringToFront: emptyFunction, + renderDepth: 1, + isContentActive: returnFalse, + whenChildContentsActiveChanged: emptyFunction, + ScreenToLocalTransform: Transform.Identity, + focus: emptyFunction, + PanelWidth: () => this.props.columnWidth, + PanelHeight: () => CollectionSchemaView._rowHeight, + addDocTab: returnFalse, + pinToPres: returnZero, + }; + + return ( + <div className="schema-table-cell" style={{ width: this.props.columnWidth }}> + <div className="schemacell-edit-wrapper" style={this.props.isRowActive() ? { cursor: 'text', pointerEvents: 'auto' } : { cursor: 'default', pointerEvents: 'none' }}> + <EditableView + contents={<FieldView {...props} />} + GetValue={() => Field.toKeyValueString(this.props.Document, this.props.fieldKey)} + SetValue={(value: string, shiftDown?: boolean, enterKey?: boolean) => { + if (shiftDown && enterKey) { + this.props.setColumnValues(this.props.fieldKey, value); + } + return KeyValueBox.SetField(this.props.Document, this.props.fieldKey, value); + }} + editing={this.props.isRowActive() ? undefined : false} + /> + </div> + </div> + ); + } +} diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 610c10be6..384975b45 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -19,6 +19,7 @@ import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; import './AudioBox.scss'; import { FieldView, FieldViewProps } from './FieldView'; +import { SelectionManager } from '../../util/SelectionManager'; import { PinProps, PresBox } from './trails'; /** diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 679c10a20..c21bc9754 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -9,6 +9,7 @@ import { DirectoryImportBox } from '../../util/Import & Export/DirectoryImportBo import { CollectionDockingView } from '../collections/CollectionDockingView'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { CollectionSchemaView } from '../collections/collectionSchema/CollectionSchemaView'; +import { SchemaRowBox } from '../collections/collectionSchema/SchemaRowBox'; import { CollectionView } from '../collections/CollectionView'; import { InkingStroke } from '../InkingStroke'; import { PresElementBox } from '../nodes/trails/PresElementBox'; @@ -266,6 +267,7 @@ export class DocumentContentsView extends React.Component< HTMLtag, ComparisonBox, LoadingBox, + SchemaRowBox, }} bindings={bindings} jsx={layoutFrame} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 9e4c21a29..f34ac2b44 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -208,6 +208,7 @@ export interface DocumentViewProps extends DocumentViewSharedProps { hideDocumentButtonBar?: boolean; hideOpenButton?: boolean; hideDeleteButton?: boolean; + hideLinkAnchors?: boolean; treeViewDoc?: Doc; isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events isContentActive: () => boolean | undefined; // whether document contents should handle pointer events @@ -614,7 +615,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } } else if (!this._longPress && this.onClickHandler?.script && !isScriptBox()) { // bcz: hack? don't execute script if you're clicking on a scripting box itself - const { clientX, clientY, shiftKey, altKey } = e; + const { clientX, clientY, shiftKey, altKey, metaKey } = e; const func = () => this.onClickHandler?.script.run( { @@ -628,6 +629,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps clientY, shiftKey, altKey, + metaKey, }, console.log ).result?.select === true @@ -1212,6 +1214,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @computed get allLinkEndpoints() { // the small blue dots that mark the endpoints of links TraceMobx(); + if (this.props.hideLinkAnchors) return null; if (this.layoutDoc.unrendered || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return null; if (this.rootDoc.type === DocumentType.PRES || this.rootDoc.type === DocumentType.LINK || this.props.dontRegisterView) return null; const filtered = DocUtils.FilterDocs(this.directLinks, this.props.docFilters?.() ?? [], []).filter(d => d.linkDisplay); diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx index ec5917b9e..6efe62e0b 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -1,32 +1,31 @@ import * as React from 'react'; -import "./RecordingView.scss"; -import { useEffect, useRef, useState } from "react"; -import { ProgressBar } from "./ProgressBar" +import './RecordingView.scss'; +import { useEffect, useRef, useState } from 'react'; +import { ProgressBar } from './ProgressBar'; import { MdBackspace } from 'react-icons/md'; import { FaCheckCircle } from 'react-icons/fa'; -import { IconContext } from "react-icons"; +import { IconContext } from 'react-icons'; import { Networking } from '../../../Network'; import { Upload } from '../../../../server/SharedMediaTypes'; import { returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils'; import { Presentation, TrackMovements } from '../../../util/TrackMovements'; export interface MediaSegment { - videoChunks: any[], - endTime: number, - startTime: number, - presentation?: Presentation, + videoChunks: any[]; + endTime: number; + startTime: number; + presentation?: Presentation; } interface IRecordingViewProps { - setResult: (info: Upload.AccessPathInfo, presentation?: Presentation) => void - setDuration: (seconds: number) => void - id: string + setResult: (info: Upload.AccessPathInfo, presentation?: Presentation) => void; + setDuration: (seconds: number) => void; + id: string; } const MAXTIME = 100000; export function RecordingView(props: IRecordingViewProps) { - const [recording, setRecording] = useState(false); const recordingTimerRef = useRef<number>(0); const [recordingTimer, setRecordingTimer] = useState(0); // unit is 0.01 second @@ -46,19 +45,16 @@ export function RecordingView(props: IRecordingViewProps) { const [finished, setFinished] = useState<boolean>(false); const [trackScreen, setTrackScreen] = useState<boolean>(false); - - const DEFAULT_MEDIA_CONSTRAINTS = { video: { width: 1280, height: 720, - }, audio: { echoCancellation: true, noiseSuppression: true, - sampleRate: 44100 - } + sampleRate: 44100, + }, }; useEffect(() => { @@ -71,12 +67,11 @@ export function RecordingView(props: IRecordingViewProps) { const videoFiles = videos.map((vid, i) => new File(vid.videoChunks, `segvideo${i}.mkv`, { type: vid.videoChunks[0].type, lastModified: Date.now() })); // upload the segments to the server and get their server access paths - const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles)) - .map(res => (res.result instanceof Error) ? '' : res.result.accessPaths.agnostic.server) + const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles)).map(res => (res.result instanceof Error ? '' : res.result.accessPaths.agnostic.server)); // concat the segments together using post call const result: Upload.AccessPathInfo | Error = await Networking.PostToServer('/concatVideos', serverPaths); - !(result instanceof Error) ? props.setResult(result, concatPres || undefined) : console.error("video conversion failed"); + !(result instanceof Error) ? props.setResult(result, concatPres || undefined) : console.error('video conversion failed'); })(); } }, [videos]); @@ -87,7 +82,9 @@ export function RecordingView(props: IRecordingViewProps) { }, [finished]); // check if the browser supports media devices on first load - useEffect(() => { if (!navigator.mediaDevices) alert('This browser does not support getUserMedia.'); }, []); + useEffect(() => { + if (!navigator.mediaDevices) alert('This browser does not support getUserMedia.'); + }, []); useEffect(() => { let interval: any = null; @@ -102,24 +99,24 @@ export function RecordingView(props: IRecordingViewProps) { }, [recording]); useEffect(() => { - setVideoProgressHelper(recordingTimer) + setVideoProgressHelper(recordingTimer); recordingTimerRef.current = recordingTimer; }, [recordingTimer]); const setVideoProgressHelper = (progress: number) => { const newProgress = (progress / MAXTIME) * 100; setProgress(newProgress); - } + }; const startShowingStream = async (mediaConstraints = DEFAULT_MEDIA_CONSTRAINTS) => { const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints); - videoElementRef.current!.src = ""; + videoElementRef.current!.src = ''; videoElementRef.current!.srcObject = stream; videoElementRef.current!.muted = true; return stream; - } + }; const record = async () => { // don't need to start a new stream every time we start recording a new segment @@ -145,29 +142,28 @@ export function RecordingView(props: IRecordingViewProps) { const nextVideo = { videoChunks, endTime: recordingTimerRef.current, - startTime: videos?.lastElement()?.endTime || 0 + startTime: videos?.lastElement()?.endTime || 0, }; // depending on if a presenation exists, add it to the video const presentation = TrackMovements.Instance.yieldPresentation(); - setVideos(videos => [...videos, (presentation != null && trackScreen) ? { ...nextVideo, presentation } : nextVideo]); + setVideos(videos => [...videos, presentation != null && trackScreen ? { ...nextVideo, presentation } : nextVideo]); } // reset the temporary chunks videoChunks = []; setRecording(false); - } + }; videoRecorder.current.start(200); - } - + }; // if this is called, then we're done recording all the segments const finish = (e: React.PointerEvent) => { e.stopPropagation(); // call stop on the video recorder if active - videoRecorder.current?.state !== "inactive" && videoRecorder.current?.stop(); + videoRecorder.current?.state !== 'inactive' && videoRecorder.current?.stop(); // end the streams (audio/video) to remove recording icon const stream = videoElementRef.current!.srcObject; @@ -178,94 +174,91 @@ export function RecordingView(props: IRecordingViewProps) { // this will call upon progessbar to update videos to be in the correct order setFinished(true); - } + }; const pause = (e: React.PointerEvent) => { e.stopPropagation(); // if recording, then this is just a new segment - videoRecorder.current?.state === "recording" && videoRecorder.current.stop(); - } + videoRecorder.current?.state === 'recording' && videoRecorder.current.stop(); + }; const start = (e: React.PointerEvent) => { - setupMoveUpEvents({}, e, returnTrue, returnFalse, e => { - // start recording if not already recording - if (!videoRecorder.current || videoRecorder.current.state === "inactive") record(); - - return true; // cancels propagation to documentView to avoid selecting it. - }, false, false); - } + setupMoveUpEvents( + {}, + e, + returnTrue, + returnFalse, + e => { + // start recording if not already recording + if (!videoRecorder.current || videoRecorder.current.state === 'inactive') record(); + + return true; // cancels propagation to documentView to avoid selecting it. + }, + false, + false + ); + }; const undoPrevious = (e: React.PointerEvent) => { e.stopPropagation(); setDoUndo(prev => !prev); - } + }; - const handleOnTimeUpdate = () => { playing && setVideoProgressHelper(videoElementRef.current!.currentTime); }; + const handleOnTimeUpdate = () => { + playing && setVideoProgressHelper(videoElementRef.current!.currentTime); + }; const millisecondToMinuteSecond = (milliseconds: number) => { const toTwoDigit = (digit: number) => { - return String(digit).length == 1 ? "0" + digit : digit - } + return String(digit).length == 1 ? '0' + digit : digit; + }; const minutes = Math.floor((milliseconds % (1000 * 60 * 60)) / (1000 * 60)); const seconds = Math.floor((milliseconds % (1000 * 60)) / 1000); - return toTwoDigit(minutes) + " : " + toTwoDigit(seconds); - } + return toTwoDigit(minutes) + ' : ' + toTwoDigit(seconds); + }; return ( <div className="recording-container"> <div className="video-wrapper"> - <video id={`video-${props.id}`} - autoPlay - muted - onTimeUpdate={() => handleOnTimeUpdate()} - ref={videoElementRef} - /> + <video id={`video-${props.id}`} autoPlay muted onTimeUpdate={() => handleOnTimeUpdate()} ref={videoElementRef} /> <div className="recording-sign"> <span className="dot" /> <p className="timer">{millisecondToMinuteSecond(recordingTimer * 10)}</p> </div> <div className="controls"> - <div className="controls-inner-container"> - <div className="record-button-wrapper"> - {recording ? - <button className="stop-button" onPointerDown={pause} /> : - <button className="record-button" onPointerDown={start} /> - } - </div> - - {!recording && (videos.length > 0 ? - - <div className="options-wrapper video-edit-wrapper"> - <IconContext.Provider value={{ color: "grey", className: "video-edit-buttons", style: { display: canUndo ? 'inherit' : 'none' } }}> - <MdBackspace onPointerDown={undoPrevious} /> - </IconContext.Provider> - <IconContext.Provider value={{ color: "#cc1c08", className: "video-edit-buttons" }}> - <FaCheckCircle onPointerDown={finish} /> - </IconContext.Provider> - </div> - - : <div className="options-wrapper track-screen-wrapper"> - <label className="track-screen"> - <input type="checkbox" checked={trackScreen} onChange={(e) => { setTrackScreen(e.target.checked) }} /> - <span className="checkmark"></span> - Track Screen - </label> - </div>)} - + <div className="record-button-wrapper">{recording ? <button className="stop-button" onPointerDown={pause} /> : <button className="record-button" onPointerDown={start} />}</div> + + {!recording && + (videos.length > 0 ? ( + <div className="options-wrapper video-edit-wrapper"> + <IconContext.Provider value={{ color: 'grey', className: 'video-edit-buttons', style: { display: canUndo ? 'inherit' : 'none' } }}> + <MdBackspace onPointerDown={undoPrevious} /> + </IconContext.Provider> + <IconContext.Provider value={{ color: '#cc1c08', className: 'video-edit-buttons' }}> + <FaCheckCircle onPointerDown={finish} /> + </IconContext.Provider> + </div> + ) : ( + <div className="options-wrapper track-screen-wrapper"> + <label className="track-screen"> + <input + type="checkbox" + checked={trackScreen} + onChange={e => { + setTrackScreen(e.target.checked); + }} + /> + <span className="checkmark"></span> + Track Screen + </label> + </div> + ))} </div> - </div> - <ProgressBar - videos={videos} - setVideos={setVideos} - orderVideos={orderVideos} - progress={progress} - recording={recording} - doUndo={doUndo} - setCanUndo={setCanUndo} - /> + <ProgressBar videos={videos} setVideos={setVideos} orderVideos={orderVideos} progress={progress} recording={recording} doUndo={doUndo} setCanUndo={setCanUndo} /> </div> - </div>) -}
\ No newline at end of file + </div> + ); +} |