Signal drop!
Relay (operand.online) is unreachable.
Usually, a dropped signal means an upgrade is happening. Hold on!
Sorry, no connección.
Hang in there while we get back on track
gram: luck
> ./src/SheetComponent.tsx
Lenses
(coming soon!)
import { Text } from "@codemirror/state";
import classNames from "classnames";
import { isArray } from "lodash";
import { action, comparer, computed, runInAction } from "mobx";
import { observer } from "mobx-react-lite";
import React, { FC, forwardRef, useEffect, useRef, useState } from "react";
import {
hoverHighlightsMobx,
isSheetExpandedMobx,
PropertyDefinition,
PropertyVisibility,
selectedTextDocumentIdBox,
SheetConfig,
sheetConfigsMobx,
SheetValueRow,
SheetView,
Span,
TextDocument,
TextDocumentSheet,
textEditorStateMobx,
textEditorViewMobx,
} from "./primitives";
import {
doSpansOverlap,
getTextForHighlight,
isHighlightComponent,
isNumericish,
isValueRowHighlight,
transformColumnFormula,
} from "./utils";
import { FORMULA_REFERENCE } from "./formulas";
import { SheetCalendar } from "./SheetCalendar";
import { HighlightHoverCard } from "./HighlightHoverCard";
import {
ArrowDownIcon,
ArrowUpIcon,
CalendarIcon,
CaretDownIcon,
CaretUpIcon,
CookieIcon,
DotsVerticalIcon,
EyeClosedIcon,
EyeNoneIcon,
EyeOpenIcon,
LetterCaseCapitalizeIcon,
MixerVerticalIcon,
PlusIcon,
QuestionMarkCircledIcon,
SectionIcon,
TableIcon,
TrashIcon,
} from "@radix-ui/react-icons";
import { NutritionLabel } from "./NutritionLabel";
import * as Popover from "@radix-ui/react-popover";
import { EditorView, minimalSetup } from "codemirror";
import { bracketMatching, LanguageSupport } from "@codemirror/language";
import { javascriptLanguage } from "@codemirror/lang-javascript";
import { highlightSpecialChars, keymap, tooltips } from "@codemirror/view";
import { autocompletion, closeBrackets, closeBracketsKeymap, } from "@codemirror/autocomplete";
import { IObservableArray } from "mobx/dist/internal";
import { getPatternExprGroupNames } from "./patterns";
import { Highlight } from "./highlight";
let i = 1;
export type SheetViewProps = {
textDocument: TextDocument;
sheetConfig: SheetConfig;
columns: PropertyDefinition[];
rows: SheetValueRow[];
rowsActiveInDoc: SheetValueRow[];
isExpanded: boolean;
};
function EditableHighlightInput({
initialText,
onUpdate,
onCancel,
}: {
initialText: string;
onUpdate: (newValue: string) => void;
onCancel: () => void;
}) {
const [value, setValue] = useState(initialText);
const inputRef = useRef<HTMLInputElement | null>(null);
return (
<span className="relative">
<input
type="text"
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
onBlur={() => {
onUpdate(value);
}}
onKeyDown={(e) => {
switch (e.key) {
case "Enter": {
onUpdate(value);
break;
}
case "Escape": {
onCancel();
break;
}
}
}}
className="absolute px-2 top-px bottom-0 left-0 right-0 bg-transparent border-0 outline-none z-1"
autoFocus={true}
ref={inputRef}
/>
<span className="px-2 invisible">{value}</span>
</span>
);
}
const EditableHighlight = forwardRef<HTMLSpanElement, { highlight: Highlight }>(
({ highlight }, forwardedRef) => {
const [isEditing, setIsEditing] = useState(false);
const text = getTextForHighlight(highlight);
return (
<span
className={classNames(
"inline-block bg-white py-0.5 border font-sans cursor-default rounded-md",
isEditing
? "border-dashed border-gray-300"
: "cm-highlight border-gray-200 hover:bg-yellow-200"
)}
ref={forwardedRef}
>
{isEditing ? (
<EditableHighlightInput
initialText={text ?? ""}
onUpdate={(newValue) => {
textEditorViewMobx.get()?.dispatch({
changes: {
from: highlight.span[0],
to: highlight.span[1],
insert: newValue,
},
});
setIsEditing(false);
}}
onCancel={() => {
setIsEditing(false);
}}
/>
) : (
<span
onClick={() => {
if (selectedTextDocumentIdBox.get() === highlight.documentId) {
setIsEditing(true);
}
}}
className="px-2"
>
{text}
</span>
)}
</span>
);
}
);
export function ValueDisplay({ value, doc }: { value: any; doc: Text }) {
if (React.isValidElement(value)) {
return value;
}
if (value instanceof Error) {
return <span className="text-red-500 font-mono">#Err</span>;
}
if (value === undefined) {
return <span className="text-gray-400 font-mono">undefined</span>;
}
if (value === null) {
return <span className="text-gray-400 font-mono">null</span>;
}
if (isHighlightComponent(value)) {
return <span className="font-mono">{value.render()}</span>;
}
if (isValueRowHighlight(value)) {
return (
<HighlightHoverCard highlight={value}>
<EditableHighlight highlight={value} />
</HighlightHoverCard>
);
}
if (isArray(value)) {
const lastIndex = value.length - 1;
return (
<span>
<span className="text-gray-400">[</span>
{value.map((item: any, index: number) =>
index === lastIndex ? (
<ValueDisplay value={item} doc={doc} key={index} />
) : (
<span key={index}>
<ValueDisplay value={item} doc={doc} />
<span className="text-gray-400">,</span>{" "}
</span>
)
)}
<span className="text-gray-400">]</span>
</span>
);
}
return <pre className="font-mono">{JSON.stringify(value, null, 2)}</pre>;
}
const SheetSettingsPopoverContent = observer(
({
textDocument,
textDocumentSheet,
sheetConfig,
}: {
textDocument: TextDocument;
textDocumentSheet: TextDocumentSheet;
sheetConfig: SheetConfig;
}) => {
return (
<div className="w-64 bg-white rounded shadow-xl text-sm overflow-hidden p-2">
<div className="">
<div className="text-sm text-gray-500">
<SectionIcon className="inline" /> Highlighting{" "}
{textDocumentSheet.highlightSearchRange === undefined
? "whole document"
: "limited range"}
{textDocumentSheet.highlightSearchRange !== undefined && (
<button
className="ml-4 underline"
onClick={() =>
runInAction(() => {
textDocumentSheet.highlightSearchRange = undefined;
})
}
>
Clear
</button>
)}
{textDocumentSheet.highlightSearchRange === undefined && (
<button
className="ml-4 underline"
onClick={() =>
runInAction(() => {
const editorState = textEditorStateMobx.get();
const from = editorState.selection.main.from;
const to = editorState.selection.main.to;
if (from !== to) {
textDocumentSheet.highlightSearchRange = [from, to];
}
})
}
>
{textEditorStateMobx.get().selection.main.from !==
textEditorStateMobx.get().selection.main.to && (
<span>Limit range to selection</span>
)}
</button>
)}
</div>
</div>
<div className="mt-2">
<button
className="flex items-center gap-1 text-red-400 hover:text-red-500"
onClick={() => {
runInAction(() => {
(
textDocument.sheets as IObservableArray<TextDocumentSheet>
).remove(textDocumentSheet);
});
}}
>
<TrashIcon /> Delete sheet
</button>
</div>
</div>
);
}
);
const SheetFormulaBar = observer(
({
textDocument,
textDocumentSheet,
sheetConfig,
isExpanded,
}: {
textDocument: TextDocument;
textDocumentSheet: TextDocumentSheet;
sheetConfig: SheetConfig;
isExpanded: boolean;
}) => {
const firstColumn = sheetConfig.properties[0];
return (
<div
className={classNames(
"bg-white flex items-center gap-2 pl-2 overflow-hidden h-8 border-b border-gray-200"
)}
>
<SearchFormulaInput
value={firstColumn.formula}
onChange={action((value) => {
sheetConfig.properties = sheetConfig.properties.map(
(column, index) =>
index === 0 ? { ...column, formula: value } : column
);
})}
/>
</div>
);
}
);
function FormulaReferenceButton({ className }: { className?: string }) {
return (
<Popover.Root>
<Popover.Anchor asChild={true}>
<Popover.Trigger asChild={true}>
<button
className={classNames(
"flex flex-shrink-0 items-center justify-center text-gray-400 hover:text-gray-600",
className
)}
>
<QuestionMarkCircledIcon />
</button>
</Popover.Trigger>
</Popover.Anchor>
<Popover.Portal>
<Popover.Content
align="end"
className="font-mono text-xs bg-gray-50 p-4 rounded shadow-lg overflow-auto max-h-[calc(100vh-256px)]"
style={{zIndex: 99999}}
>
<div className="uppercase mb-2">available formulas</div>
<table>
<tbody>
{FORMULA_REFERENCE.map(({ name, args, return: returnType }) => (
<tr className="border-t border-gray-200" key={name}>
<td className="py-1 pr-2">{name}</td>
<td className="text-gray-400 pr-2">
<span className="text-gray-400">(</span>
{args.join(", ")}
<span className="text-gray-400">)</span>
</td>
<td className="text-gray-400">{returnType}</td>
</tr>
))}
</tbody>
</table>
</Popover.Content>
</Popover.Portal>
</Popover.Root>
);
}
const textEditorSelectionSpanComputed = computed<Span>(() => {
const selectionRange = textEditorStateMobx.get().selection.asSingle().main;
return [selectionRange.from, selectionRange.to] as Span;
});
enum SortMethod {
Date,
Alphabetical,
Numeric,
}
function compareColumnValues(
a: SheetValueRow,
b: SheetValueRow,
columnName: string,
sortMethod: SortMethod,
direction: "asc" | "desc"
) {
const aValue = isValueRowHighlight(a.data[columnName])
? getTextForHighlight(a.data[columnName])
: a.data[columnName];
const bValue = isValueRowHighlight(b.data[columnName])
? getTextForHighlight(b.data[columnName])
: b.data[columnName];
let rv = 0;
if (aValue === undefined) {
rv = bValue === undefined ? 0 : 1;
} else if (bValue === undefined) {
rv = -1;
} else {
switch (sortMethod) {
case SortMethod.Date: {
rv = new Date(aValue).getTime() - new Date(bValue).getTime();
break;
}
case SortMethod.Numeric: {
rv = parseFloat(aValue) - parseFloat(bValue);
break;
}
case SortMethod.Alphabetical: {
rv = String(aValue).localeCompare(bValue);
break;
}
}
}
if (direction === "desc") {
rv = -rv;
}
return rv === 0
? isValueRowHighlight(a) && isValueRowHighlight(b)
? a.span[0] - b.span[0]
: 0
: rv;
}
function useFormulaInput(
rootRef: React.MutableRefObject<HTMLDivElement | null>,
value: string,
onChange: (value: string) => void,
cmContentTheme: any,
useLineWrapping = false,
disableAutocomplete = false
): React.MutableRefObject<EditorView | undefined> {
const viewRef = useRef<EditorView | undefined>(undefined);
const valueRef = useRef(value);
useEffect(() => {
const view = new EditorView({
doc: value,
extensions: [
minimalSetup,
EditorView.theme({
".cm-content": cmContentTheme,
".cm-completionIcon": {
width: "1em",
},
".cm-completionLabel": {
fontFamily: `ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace`,
fontSize: "12px",
},
".cm-completionInfo": {
fontFamily: `ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace`,
fontSize: "12px",
},
}),
highlightSpecialChars(),
bracketMatching(),
closeBrackets(),
autocompletion(),
tooltips({ parent: document.body }),
useLineWrapping ? [EditorView.lineWrapping] : [],
!disableAutocomplete
? new LanguageSupport(javascriptLanguage, [
javascriptLanguage.data.of({
autocomplete: FORMULA_REFERENCE.map(
({ name, args, return: returnType }) => ({
label: name,
type: "function",
info: `(${args.join(", ")}) => ${returnType}`,
})
),
}),
])
: [],
keymap.of([...closeBracketsKeymap]),
],
parent: rootRef.current!,
dispatch(transaction) {
view.update([transaction]);
if (transaction.docChanged) {
const value = transaction.state.doc.toString();
valueRef.current = value;
onChange(value);
}
},
});
viewRef.current = view;
return () => {
view.destroy();
};
}, []);
useEffect(() => {
if (valueRef.current !== value) {
const view = viewRef.current!;
view.dispatch({
changes: {
from: 0,
to: view.state.doc.length,
insert: value,
},
});
}
}, [value]);
return viewRef;
}
function SearchFormulaInput({
value,
onChange,
}: {
value: string;
onChange: (value: string) => void;
}) {
const rootRef = useRef<HTMLDivElement>(null);
useFormulaInput(
rootRef,
value,
onChange,
{
fontSize: "14px",
fontFamily: `ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace`,
},
false,
true
);
return <div className="grow overflow-auto" ref={rootRef}></div>;
}
function FormulaInput({
value,
onChange,
}: {
value: string;
onChange: (value: string) => void;
}) {
const rootRef = useRef<HTMLDivElement>(null);
const editorViewRef = useFormulaInput(
rootRef,
value,
onChange,
{
padding: "4px 4px 4px 20px",
fontSize: "12px",
fontFamily: `ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace`,
minHeight: "48px",
maxHeight: "96px",
},
true
);
useEffect(() => {
editorViewRef.current?.focus();
}, []);
return (
<div className="relative">
<div
className="border-2 border-blue-500 overflow-auto rounded-t"
ref={rootRef}
></div>
<div className="absolute top-0 left-0 px-1 py-0.5 bg-blue-500 text-white text-sm italic rounded-br">
fx
</div>
</div>
);
}
const SheetColumnSettingsPopoverContent = observer(
({
sheetConfig,
columnIndex,
column,
}: {
sheetConfig: SheetConfig;
columnIndex: number;
column: PropertyDefinition;
}) => {
const changeFormulaAt = action((changedIndex: number, formula: string) => {
sheetConfig.properties = sheetConfig.properties.map((column, index) =>
index === changedIndex ? { ...column, formula } : column
);
});
const changeNameAt = action((changedIndex: number, name: string) => {
sheetConfig.properties = sheetConfig.properties.map((column, index) =>
index === changedIndex ? { ...column, name } : column
);
});
const readableNamesForPropertyVisibility: {
[key in PropertyVisibility]: string;
} = {
[PropertyVisibility.Hidden]: "Hidden",
[PropertyVisibility.Inline]: "Next to text",
[PropertyVisibility.Superscript]: "Above text",
[PropertyVisibility.Replace]: "Replace text",
[PropertyVisibility.Style]: "Apply CSS style",
};
return (
<div className="w-80 bg-white rounded shadow-xl text-sm overflow-hidden">
<div className="relative">
<FormulaInput
value={column.formula}
onChange={(value) => {
changeFormulaAt(columnIndex, value);
}}
/>
<FormulaReferenceButton className="absolute bottom-1 left-1" />
</div>
<div className="p-2">
<div className="flex items-center justify-between gap-2 mb-1">
<LetterCaseCapitalizeIcon className="inline" />
Name
<input
className="block grow pl-1 py-0.5 self-stretch bg-orange-50 border border-orange-100 text-orange-500 font-medium rounded-sm"
value={column.name}
onChange={(evt) => changeNameAt(columnIndex, evt.target.value)}
/>
</div>
<div className="flex items-center mb-2">
<div>
{column.visibility === PropertyVisibility.Hidden ? (
<EyeClosedIcon className="inline text-gray-500" />
) : (
<EyeOpenIcon className="inline text-gray-500" />
)}{" "}
Display in document:
</div>
<select
value={column.visibility}
onChange={action(
(e) =>
(column.visibility = e.target.value as PropertyVisibility)
)}
className="text-blue-500"
>
{Object.values(PropertyVisibility).map((value, i) => (
<option value={value} key={i}>
{readableNamesForPropertyVisibility[value]}
</option>
))}
</select>
</div>
{/* We put the delete button inside the Radix close button so the popover closes upon deleting a*/}
<Popover.Close
className="text-gray-400 text-xs hover:text-gray-800 p-1"
onClick={() => {
runInAction(() => {
(
sheetConfig.properties as IObservableArray<PropertyDefinition>
).remove(column);
});
}}
>
<TrashIcon className="inline" /> Delete property
</Popover.Close>
</div>
</div>
);
}
);
function SheetTableColumnSettingsButton({
sheetConfig,
column,
columnIndex,
defaultOpen,
}: {
sheetConfig: SheetConfig;
column: PropertyDefinition;
columnIndex: number;
defaultOpen: boolean;
}) {
const [open, setOpen] = useState(false);
useEffect(() => {
if (defaultOpen) {
// without this timeout, popover would show up in top-left viewport
// seems like issue with opening popover on mount
setTimeout(() => {
setOpen(true);
}, 50);
}
}, []);
return (
<Popover.Root open={open} onOpenChange={setOpen} modal={true}>
<Popover.Anchor asChild={true}>
<Popover.Trigger asChild={true}>
<button className="text-gray-400 hover:text-gray-500">
<MixerVerticalIcon />
</button>
</Popover.Trigger>
</Popover.Anchor>
<Popover.Content side="top" sideOffset={8} align="end" alignOffset={-8}>
<SheetColumnSettingsPopoverContent
sheetConfig={sheetConfig}
columnIndex={columnIndex}
column={column}
/>
</Popover.Content>
</Popover.Root>
);
}
export const SheetTable = observer(
({
textDocument,
sheetConfig,
columns,
rows,
rowsActiveInDoc,
isExpanded,
}: SheetViewProps) => {
const [sortBy, setSortBy] = useState<
{ columnName: string; direction: "asc" | "desc" } | undefined
>(undefined);
const [didJustAddColumn, setDidJustAddColumn] = useState(false);
const addColumn = action(() => {
sheetConfig.properties.push({
name: `col${++i}`,
formula: "",
visibility: PropertyVisibility.Hidden,
});
setDidJustAddColumn(true);
});
useEffect(() => {
setDidJustAddColumn(false);
}, [didJustAddColumn]);
let sortedRows = rows;
if (sortedRows.length > 0 && sortBy !== undefined) {
const { columnName, direction } = sortBy;
const firstRow = sortedRows[0];
const firstRowColumnValue = isValueRowHighlight(firstRow.data[columnName])
? getTextForHighlight(firstRow.data[columnName])
: firstRow.data[columnName];
const sortMethod =
columnName === "date"
? SortMethod.Date
: isNumericish(firstRowColumnValue)
? SortMethod.Numeric
: SortMethod.Alphabetical;
sortedRows = [...rows].sort((a, b) =>
compareColumnValues(a, b, columnName, sortMethod, direction)
);
}
if (!isExpanded) {
sortedRows = sortedRows.slice(0, 2);
}
const headFormula = transformColumnFormula(columns[0].formula, true);
const groupNames = getPatternExprGroupNames(headFormula);
const groupColumnsOffset = groupNames.length;
const columnsWithPatternGroups = columns
.slice(0, 1)
.concat(
groupNames.map((name) => ({
name,
isPatternGroup: true,
formula: "",
visibility: PropertyVisibility.Hidden,
}))
)
.concat(columns.slice(1));
return (
<>
<div className="max-h-[250px] overflow-auto relative w-full flex text-sm">
{!isExpanded && (
<div className="absolute bottom-0 h-12 w-full bg-gradient-to-t from-gray-200 z-10"></div>
)}
<table className="flex-1">
<thead>
<tr
className="sticky h-[31px] top-0 bg-gray-100"
style={{ zIndex: 1 }}
>
{columnsWithPatternGroups.map((column, index) => {
const isEditable = index !== 0 && !column.isPatternGroup;
return (
<th
key={index}
className={classNames(
"border-b border-r border-gray-200 text-left font-normal pl-1.5",
index !== 0 ? "border-l" : undefined
)}
>
<div className="flex gap-1 pr-1 items-center justify-between">
<div
className={classNames(
"px-1 rounded-sm font-medium text-xs border",
isEditable
? "bg-orange-100 text-orange-500 border-orange-200"
: "bg-indigo-100 text-indigo-500 border-indigo-200"
)}
>
{column.name}
</div>
{isEditable ? (
<>
<SheetTableColumnSettingsButton
sheetConfig={sheetConfig}
columnIndex={index - groupColumnsOffset}
column={column}
defaultOpen={
didJustAddColumn &&
index === columnsWithPatternGroups.length - 1
}
/>
<div className="hidden">
<button
onClick={(e) => {
e.stopPropagation();
if (
sortBy?.columnName === column.name &&
sortBy.direction === "asc"
) {
setSortBy(undefined);
} else {
setSortBy({
columnName: column.name,
direction: "asc",
});
}
}}
className={classNames(
sortBy?.columnName === column.name &&
sortBy.direction === "asc"
? "opacity-100"
: "opacity-20 hover:opacity-60"
)}
>
<ArrowDownIcon />
</button>
<button
onClick={(e) => {
e.stopPropagation();
if (
sortBy?.columnName === column.name &&
sortBy.direction === "desc"
) {
setSortBy(undefined);
} else {
setSortBy({
columnName: column.name,
direction: "desc",
});
}
}}
className={classNames(
sortBy?.columnName === column.name &&
sortBy.direction === "desc"
? "opacity-100"
: "opacity-20 hover:opacity-60"
)}
>
<ArrowUpIcon />
</button>
</div>
</>
) : null}
</div>
</th>
);
})}
<th className="bg-gray-50 w-[28px]">
<button
onClick={() => addColumn()}
className="flex h-[25px] items-center justify-center w-full opacity-50 hover:opacity-100"
>
<PlusIcon />
</button>
</th>
</tr>
</thead>
<tbody>
{sortedRows.map((row, rowIndex) => (
<tr
onMouseEnter={action(() => {
const childrenHighlights = Object.values(row.data).flatMap(
(columnData) =>
isValueRowHighlight(columnData) ? [columnData] : []
);
hoverHighlightsMobx.replace(
childrenHighlights.length > 0
? childrenHighlights
: isValueRowHighlight(row)
? [row]
: []
);
})}
onMouseLeave={action(() => {
hoverHighlightsMobx.clear();
})}
className={classNames(
"hover:bg-blue-50",
rowsActiveInDoc.includes(row) ? "bg-blue-100" : "bg-gray-50"
)}
key={rowIndex}
>
{columnsWithPatternGroups.map((column, colIndex) => {
const value: any = row.data[column.name];
return (
<td
className={classNames(
"border border-gray-200 px-1 py-1 relative min-h-[32px]",
colIndex === 0 ? "border-l-transparent" : undefined,
rowIndex === sortedRows.length - 1
? "border-b-transparent"
: undefined
)}
key={colIndex}
>
<ValueDisplay value={value} doc={textDocument.text} />
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
</>
);
}
);
export const SheetComponent = observer(
({
id,
textDocument,
textDocumentSheetId,
rows,
}: {
id: string;
textDocument: TextDocument;
textDocumentSheetId: string;
rows: SheetValueRow[];
}) => {
const [sheetView, setSheetView] = useState(SheetView.Table);
const textDocumentSheet = textDocument.sheets.find(
(sheet) => sheet.id === textDocumentSheetId
)!;
const sheetConfig = sheetConfigsMobx.get(textDocumentSheet.configId);
// HACK: for demo automatically expand calendar view if search is opened
useEffect(() => {
if (!sheetConfig) {
return
}
if (textDocument.id === "workout" && sheetConfig.name === 'exercise') {
isSheetExpandedMobx.set(textDocumentSheet.id, true)
setSheetView(SheetView.Calendar)
}
}, [textDocument.id, sheetConfig?.name])
if (sheetConfig === undefined) {
return null;
}
const columns = sheetConfig.properties;
const SheetViewComponent: FC<SheetViewProps> = {
[SheetView.Table]: SheetTable,
[SheetView.Calendar]: SheetCalendar,
[SheetView.NutritionLabel]: NutritionLabel,
}[sheetView]!;
const canRenderAsCalendar = sheetConfig.properties.some(
(column) => column.name === "date"
);
const canRenderAsNutritionLabel = sheetConfig.name === "ingredients";
const rowsActiveInDoc = computed(
() =>
rows.filter(
(row) =>
isValueRowHighlight(row) &&
doSpansOverlap(row.span, textEditorSelectionSpanComputed.get())
),
{ equals: comparer.shallow }
).get();
// If the user has selected a highlight in the doc that is from this sheet,
// we temporarily expand the sheet to show the highlight.
const temporarilyExpanded =
(rowsActiveInDoc.length > 0 || rows.length <= 2) &&
!textDocumentSheet.hideHighlightsInDocument;
const isExpanded = isSheetExpandedMobx.get(id) || temporarilyExpanded;
const toggleIsExpanded = action(() => {
isSheetExpandedMobx.set(textDocumentSheet.id, !isExpanded);
});
return (
<div className="z-10">
<div className="pl-8 flex items-center justify-between mb-2">
<input
type="text"
value={sheetConfig.name}
onChange={action((e) => {
sheetConfig.name = e.target.value;
})}
className="text-xs font-semibold outline-none bg-transparent text-gray-500 focus:text-gray-600"
/>
{isExpanded && (canRenderAsCalendar || canRenderAsNutritionLabel) ? (
<div className="flex gap-2 pr-1">
<button
onClick={() => {
setSheetView(SheetView.Table);
}}
className={classNames(
"transition text-sm",
sheetView !== SheetView.Table
? "opacity-40 hover:opacity-100"
: undefined
)}
>
<TableIcon />
</button>
{canRenderAsCalendar ? (
<button
onClick={() => {
setSheetView(SheetView.Calendar);
}}
className={classNames(
"transition text-sm",
sheetView !== SheetView.Calendar
? "opacity-40 hover:opacity-100"
: undefined
)}
>
<CalendarIcon />
</button>
) : null}
{canRenderAsNutritionLabel ? (
<button
onClick={() => {
setSheetView(SheetView.NutritionLabel);
}}
className={classNames(
"transition text-sm",
sheetView !== SheetView.NutritionLabel
? "opacity-40 hover:opacity-100"
: undefined
)}
>
<CookieIcon />
</button>
) : null}
</div>
) : null}
</div>
<div className="flex">
<div className="w-8 flex-shrink-0">
<div
className={classNames(
"rounded-tl bg-gray-100 h-[33px] border-l border-y border-gray-200",
!isExpanded ? "rounded-bl" : undefined
)}
>
<button
className="text-gray-400 hover:text-gray-500 flex w-full h-full items-center justify-center"
onClick={() => {
runInAction(
() =>
(textDocumentSheet.hideHighlightsInDocument =
!textDocumentSheet.hideHighlightsInDocument)
);
}}
>
{textDocumentSheet.hideHighlightsInDocument ? (
<EyeNoneIcon />
) : (
<EyeOpenIcon />
)}
</button>
</div>
<div className="rounded-bl bg-gray-100 h-[31px] border-b border-l border-gray-200">
<Popover.Root modal={true}>
<Popover.Anchor asChild={true}>
<Popover.Trigger asChild={true}>
<button className="text-gray-400 hover:text-gray-600 flex w-full h-full items-center justify-center">
<DotsVerticalIcon />
</button>
</Popover.Trigger>
</Popover.Anchor>
<Popover.Portal>
<Popover.Content side="bottom" sideOffset={4} align="start">
<SheetSettingsPopoverContent
textDocument={textDocument}
textDocumentSheet={textDocumentSheet}
sheetConfig={sheetConfig}
/>
</Popover.Content>
</Popover.Portal>
</Popover.Root>
</div>
</div>
<div
className={classNames(
"flex flex-col flex-1 bg-gray-50 border border-gray-200 rounded-r overflow-hidden",
isExpanded ? "rounded-bl" : undefined
)}
>
<SheetFormulaBar
textDocument={textDocument}
textDocumentSheet={textDocumentSheet}
sheetConfig={sheetConfig}
isExpanded={isExpanded}
/>
<SheetViewComponent
textDocument={textDocument}
sheetConfig={sheetConfig}
columns={columns}
rows={rows}
rowsActiveInDoc={rowsActiveInDoc}
isExpanded={isExpanded}
/>
</div>
</div>
<div
className="text-xs whitespace-nowrap mt-1 ml-8 text-gray-400 cursor-pointer"
onClick={toggleIsExpanded}
>
{!isExpanded && (
<span>
<CaretDownIcon className="inline" /> Show all {rows.length} result
{rows.length !== 1 ? "s" : ""}
</span>
)}
{isExpanded && !temporarilyExpanded && (
<span>
<CaretUpIcon className="inline" /> Collapse
</span>
)}
</div>
</div>
);
}
);