aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2023-05-10 10:08:50 -0400
committerbobzel <zzzman@gmail.com>2023-05-10 10:08:50 -0400
commitebb846116af9c7e65a9d674c765c71c0bf0a7d29 (patch)
treed8278e7c27f7577eb5b4706604144940d12a718e /src/client/views/collections/collectionSchema/SchemaTableCell.tsx
parentf912a233a89c8772b22b71d34830ff4b0ba82310 (diff)
parent719da9462f02fd3afda9b0b65de19de9405ab4fc (diff)
Merge branch 'master' into UI_Update_Eric_Ma
Diffstat (limited to 'src/client/views/collections/collectionSchema/SchemaTableCell.tsx')
-rw-r--r--src/client/views/collections/collectionSchema/SchemaTableCell.tsx297
1 files changed, 297 insertions, 0 deletions
diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
new file mode 100644
index 000000000..712bd4491
--- /dev/null
+++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
@@ -0,0 +1,297 @@
+import React = require('react');
+import { action, computed, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import { extname } from 'path';
+import DatePicker from 'react-datepicker';
+import { DateField } from '../../../../fields/DateField';
+import { Doc, DocListCast, Field } from '../../../../fields/Doc';
+import { BoolCast, Cast, DateCast, FieldValue } from '../../../../fields/Types';
+import { ImageField } from '../../../../fields/URLField';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero, Utils } from '../../../../Utils';
+import { FInfo } from '../../../documents/Documents';
+import { dropActionType } from '../../../util/DragManager';
+import { Transform } from '../../../util/Transform';
+import { undoBatch } from '../../../util/UndoManager';
+import { EditableView } from '../../EditableView';
+import { Colors } from '../../global/globalEnums';
+import { FieldView, FieldViewProps } from '../../nodes/FieldView';
+import { KeyValueBox } from '../../nodes/KeyValueBox';
+import { DefaultStyleProvider } from '../../StyleProvider';
+import { CollectionSchemaView, ColumnType, FInfotoColType } from './CollectionSchemaView';
+import './CollectionSchemaView.scss';
+import { RichTextField } from '../../../../fields/RichTextField';
+import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
+
+export interface SchemaTableCellProps {
+ Document: Doc;
+ col: number;
+ deselectCell: () => void;
+ selectCell: (doc: Doc, col: number) => void;
+ selectedCell: () => [Doc, number] | undefined;
+ fieldKey: string;
+ columnWidth: () => number;
+ isRowActive: () => boolean | undefined;
+ getFinfo: (fieldKey: string) => FInfo | undefined;
+ setColumnValues: (field: string, value: string) => boolean;
+}
+
+@observer
+export class SchemaTableCell extends React.Component<SchemaTableCellProps> {
+ public static colRowHeightFunc() {
+ return CollectionSchemaView._rowHeight;
+ }
+ public static renderProps(props: SchemaTableCellProps) {
+ const { Document, fieldKey, getFinfo, columnWidth, isRowActive } = props;
+ let protoCount = 0;
+ let doc: Doc | undefined = Document;
+ while (doc) {
+ if (Object.keys(doc).includes(fieldKey.replace(/^_/, ''))) {
+ break;
+ }
+ protoCount++;
+ doc = doc.proto;
+ }
+ const parenCount = Math.max(0, protoCount - 1);
+ const color = protoCount === 0 || (fieldKey.startsWith('_') && Document[fieldKey] === undefined) ? 'black' : 'blue';
+ const textDecoration = color !== 'black' && parenCount ? 'underline' : '';
+ const fieldProps: FieldViewProps = {
+ docFilters: returnEmptyFilter,
+ docRangeFilters: returnEmptyFilter,
+ searchFilterDocs: returnEmptyDoclist,
+ styleProvider: DefaultStyleProvider,
+ docViewPath: returnEmptyDoclist,
+ rootSelected: returnFalse,
+ isSelected: returnFalse,
+ setHeight: returnFalse,
+ select: emptyFunction,
+ dropAction: 'alias' as dropActionType,
+ bringToFront: emptyFunction,
+ renderDepth: 1,
+ isContentActive: returnFalse,
+ whenChildContentsActiveChanged: emptyFunction,
+ ScreenToLocalTransform: Transform.Identity,
+ focus: emptyFunction,
+ addDocTab: returnFalse,
+ pinToPres: returnZero,
+ Document,
+ fieldKey,
+ PanelWidth: columnWidth,
+ PanelHeight: SchemaTableCell.colRowHeightFunc,
+ };
+ const readOnly = getFinfo(fieldKey)?.readOnly ?? false;
+ const cursor = !readOnly ? 'text' : 'default';
+ const pointerEvents: 'all' | 'none' = !readOnly && isRowActive() ? 'all' : 'none';
+ return { color, textDecoration, fieldProps, cursor, pointerEvents };
+ }
+
+ @computed get selected() {
+ const selected: [Doc, number] | undefined = this.props.selectedCell();
+ return this.props.isRowActive() && selected?.[0] === this.props.Document && selected[1] === this.props.col;
+ }
+
+ @computed get defaultCellContent() {
+ const { color, textDecoration, fieldProps } = SchemaTableCell.renderProps(this.props);
+
+ return (
+ <div
+ className="schemacell-edit-wrapper"
+ style={{
+ color,
+ textDecoration,
+ }}>
+ <EditableView
+ contents={<FieldView {...fieldProps} />}
+ editing={this.selected ? undefined : false}
+ GetValue={() => Field.toKeyValueString(this.props.Document, this.props.fieldKey)}
+ SetValue={undoBatch((value: string, shiftDown?: boolean, enterKey?: boolean) => {
+ if (shiftDown && enterKey) {
+ this.props.setColumnValues(this.props.fieldKey.replace(/^_/, ''), value);
+ }
+ return KeyValueBox.SetField(this.props.Document, this.props.fieldKey.replace(/^_/, ''), value);
+ })}
+ />
+ </div>
+ );
+ }
+
+ get getCellType() {
+ const cellValue = this.props.Document[this.props.fieldKey];
+ if (cellValue instanceof ImageField) return ColumnType.Image;
+ if (cellValue instanceof DateField) return ColumnType.Date;
+ if (cellValue instanceof RichTextField) return ColumnType.RTF;
+ if (typeof cellValue === 'number') return ColumnType.Any;
+ if (typeof cellValue === 'string') return ColumnType.Any;
+ if (typeof cellValue === 'boolean') return ColumnType.Any;
+
+ const columnTypeStr = this.props.getFinfo(this.props.fieldKey)?.fieldType;
+ if (columnTypeStr && columnTypeStr in FInfotoColType) {
+ return FInfotoColType[columnTypeStr];
+ }
+
+ return ColumnType.Any;
+ }
+
+ get content() {
+ const cellType: ColumnType = this.getCellType;
+ // prettier-ignore
+ switch (cellType) {
+ case ColumnType.Image: return <SchemaImageCell {...this.props} />;
+ case ColumnType.Boolean: return <SchemaBoolCell {...this.props} />;
+ case ColumnType.RTF: return <SchemaRTFCell {...this.props} />;
+ case ColumnType.Date: // return <SchemaDateCell {...this.props} />;
+ default: return this.defaultCellContent;
+ }
+ }
+
+ render() {
+ return (
+ <div
+ className="schema-table-cell"
+ onPointerDown={action(e => !this.selected && this.props.selectCell(this.props.Document, this.props.col))}
+ style={{ width: this.props.columnWidth(), border: this.selected ? `solid 2px ${Colors.MEDIUM_BLUE}` : undefined }}>
+ {this.content}
+ </div>
+ );
+ }
+}
+
+// mj: most of this is adapted from old schema code so I'm not sure what it does tbh
+@observer
+export class SchemaImageCell extends React.Component<SchemaTableCellProps> {
+ @observable _previewRef: HTMLImageElement | undefined;
+
+ 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, '_s' + ext);
+ }
+
+ get url() {
+ const field = Cast(this.props.Document[this.props.fieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc
+ const alts = DocListCast(this.props.Document[this.props.fieldKey + '-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')];
+ return url[0];
+ }
+
+ @action
+ showHoverPreview = (e: React.PointerEvent) => {
+ this._previewRef = document.createElement('img');
+ document.body.appendChild(this._previewRef);
+ const ext = extname(this.url);
+ this._previewRef.src = this.url.replace('_s' + ext, '_m' + ext);
+ this._previewRef.style.position = 'absolute';
+ this._previewRef.style.left = e.clientX + 10 + 'px';
+ this._previewRef.style.top = e.clientY + 10 + 'px';
+ this._previewRef.style.zIndex = '1000';
+ };
+
+ @action
+ moveHoverPreview = (e: React.PointerEvent) => {
+ if (!this._previewRef) return;
+ this._previewRef.style.left = e.clientX + 10 + 'px';
+ this._previewRef.style.top = e.clientY + 10 + 'px';
+ };
+
+ @action
+ removeHoverPreview = (e: React.PointerEvent) => {
+ if (!this._previewRef) return;
+ document.body.removeChild(this._previewRef);
+ };
+
+ render() {
+ const aspect = Doc.NativeAspect(this.props.Document); // aspect ratio
+ // let width = Math.max(75, this.props.columnWidth); // get a with that is no smaller than 75px
+ // const height = Math.max(75, width / aspect); // get a height either proportional to that or 75 px
+ const height = CollectionSchemaView._rowHeight - 10;
+ const width = height * aspect; // increase the width of the image if necessary to maintain proportionality
+
+ return <img src={this.url} width={width} height={height} style={{}} draggable="false" onPointerEnter={this.showHoverPreview} onPointerMove={this.moveHoverPreview} onPointerLeave={this.removeHoverPreview} />;
+ }
+}
+
+@observer
+export class SchemaDateCell extends React.Component<SchemaTableCellProps> {
+ @observable _pickingDate: boolean = false;
+
+ @computed get date(): DateField {
+ // if the cell is a date field, cast then contents to a date. Otherrwwise, make the contents undefined.
+ return DateCast(this.props.Document[this.props.fieldKey]);
+ }
+
+ @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.props.Document[this.props.fieldKey] = new DateField(date as Date);
+ //}
+ };
+
+ render() {
+ return <DatePicker dateFormat={'Pp'} selected={this.date.date} onChange={(date: any) => this.handleChange(date)} />;
+ }
+}
+@observer
+export class SchemaRTFCell extends React.Component<SchemaTableCellProps> {
+ @computed get selected() {
+ const selected: [Doc, number] | undefined = this.props.selectedCell();
+ return this.props.isRowActive() && selected?.[0] === this.props.Document && selected[1] === this.props.col;
+ }
+ selectedFunc = () => this.selected;
+ render() {
+ const { color, textDecoration, fieldProps, cursor, pointerEvents } = SchemaTableCell.renderProps(this.props);
+ fieldProps.isContentActive = this.selectedFunc;
+ return (
+ <div className="schemaRTFCell" style={{ display: 'flex', fontStyle: this.selected ? undefined : 'italic', width: '100%', height: '100%', position: 'relative', color, textDecoration, cursor, pointerEvents }}>
+ {this.selected ? <FormattedTextBox {...fieldProps} /> : (field => (field ? Field.toString(field) : ''))(FieldValue(fieldProps.Document[fieldProps.fieldKey]))}
+ </div>
+ );
+ }
+}
+@observer
+export class SchemaBoolCell extends React.Component<SchemaTableCellProps> {
+ @computed get selected() {
+ const selected: [Doc, number] | undefined = this.props.selectedCell();
+ return this.props.isRowActive() && selected?.[0] === this.props.Document && selected[1] === this.props.col;
+ }
+ render() {
+ const { color, textDecoration, fieldProps, cursor, pointerEvents } = SchemaTableCell.renderProps(this.props);
+ return (
+ <div className="schemaBoolCell" style={{ display: 'flex', color, textDecoration, cursor, pointerEvents }}>
+ <input
+ style={{ marginRight: 4 }}
+ type="checkbox"
+ checked={BoolCast(this.props.Document[this.props.fieldKey])}
+ onChange={undoBatch((value: React.ChangeEvent<HTMLInputElement> | undefined) => {
+ if ((value?.nativeEvent as any).shiftKey) {
+ this.props.setColumnValues(this.props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + value?.target?.checked.toString());
+ }
+ KeyValueBox.SetField(this.props.Document, this.props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + value?.target?.checked.toString());
+ })}
+ />
+ <EditableView
+ contents={<FieldView {...fieldProps} />}
+ editing={this.selected ? undefined : false}
+ GetValue={() => Field.toKeyValueString(this.props.Document, this.props.fieldKey)}
+ SetValue={undoBatch((value: string, shiftDown?: boolean, enterKey?: boolean) => {
+ if (shiftDown && enterKey) {
+ this.props.setColumnValues(this.props.fieldKey.replace(/^_/, ''), value);
+ }
+ return KeyValueBox.SetField(this.props.Document, this.props.fieldKey.replace(/^_/, ''), value);
+ })}
+ />
+ </div>
+ );
+ }
+}