UNPKG

@airplane/views

Version:

A React library for building Airplane views. Views components are optimized in style and functionality to produce internal apps that are easy to build and maintain.

389 lines (388 loc) 13.9 kB
import { jsx, jsxs } from "react/jsx-runtime"; import { JsonInput, useMantineTheme } from "@mantine/core"; import * as React from "react"; import { useState, useEffect, useCallback } from "react"; import { assertNever } from "../../assertNever.js"; import { CheckboxComponent } from "../checkbox/Checkbox.js"; import { Code } from "../code/Code.js"; import { DatePickerComponent } from "../datepicker/DatePicker.js"; import { formatDatetime, DateTimePickerComponent } from "../datepicker/DateTimePicker.js"; import { PencilSquareIconOutline } from "@airplane/views/icons/index.js"; import { Link } from "../link/Link.js"; import { NumberInputComponent } from "../number/NumberInput.js"; import { SelectComponent } from "../select/Select.js"; import { TextareaComponent } from "../textarea/Textarea.js"; import { useStyles } from "./Cell.styles.js"; import { useClickOutside } from "./useClickOutside.js"; import { OverflowText } from "./useIsOverflow.js"; import useKeyPress from "./useKeyPress.js"; const CellComponent = ({ value: initialValue, row, column: { id, canEdit, type, wrap, Component, typeOptions, EditComponent }, updateData, dirtyCells }) => { var _a, _b; const [value, setValue] = useState(initialValue); const [editing, setEditing] = useState(false); const { classes, cx } = useStyles(); useEffect(() => { setValue(initialValue); }, [initialValue]); const onChange = useCallback((v, shouldUpdateTable) => { setValue(v); if (shouldUpdateTable) { updateData(row, id, v); } }, [id, row, updateData]); const startEditing = useCallback(() => setEditing(true), [setEditing]); const finishEditing = useCallback((newValue) => { setEditing(false); updateData(row, id, newValue); }, [setEditing, updateData, id, row]); if (Component && !editing) { return /* @__PURE__ */ jsx("div", { className: cx(classes.cellPadding, classes.cell), children: /* @__PURE__ */ jsx(Component, { value, row: row.original, startEditing: canEdit ? startEditing : void 0 }) }); } const cellType = type || getDefaultCellType(value, typeOptions); const cancelEdit = () => setValue(initialValue); if (editing || canEdit && alwaysEditingCellTypes.includes(cellType)) { if (EditComponent) { return /* @__PURE__ */ jsx("div", { className: cx(classes.cellPadding, classes.cell), children: /* @__PURE__ */ jsx(EditComponent, { defaultValue: value, finishEditing }) }); } return /* @__PURE__ */ jsx(EditableCell, { value, onChange, type: cellType, typeOptions, isDirty: (_a = dirtyCells[row.id]) == null ? void 0 : _a.has(id), onStopEditing: (cancel) => { setEditing(false); if (cancel) { cancelEdit(); } else { updateData(row, id, value); } } }); } return /* @__PURE__ */ jsx(StaticCell, { value, type: cellType, typeOptions, wrap, onEdit: canEdit ? () => setEditing(true) : void 0, isDirty: (_b = dirtyCells[row.id]) == null ? void 0 : _b.has(id) }); }; const Cell = /* @__PURE__ */ React.memo(CellComponent, ({ value: prevValue, row: prevRow, column: prevColumn, updateData: prevUpdateData, dirtyCells: prevDirtyCells }, { value, row, column, updateData, dirtyCells }) => { const { id, canEdit, type, wrap, Component } = column; const { id: prevID, canEdit: prevCanEdit, type: prevType, wrap: prevWrap, Component: PrevComponent } = prevColumn; return value === prevValue && updateData === prevUpdateData && id === prevID && canEdit === prevCanEdit && wrap === prevWrap && Component === PrevComponent && type === prevType && row === prevRow && dirtyCells === prevDirtyCells; }); const StaticCell = (props) => { const { onEdit, isDirty } = props; const { classes, cx } = useStyles(); return /* @__PURE__ */ jsxs("div", { className: cx(classes.cell, { [classes.dirty]: isDirty }), "data-testid": `static-cell${isDirty ? "-dirty" : ""}`, children: [ /* @__PURE__ */ jsx(StaticCellValue, { ...props }), onEdit && /* @__PURE__ */ jsx("div", { className: "cellEditIcon", children: /* @__PURE__ */ jsx(PencilSquareIconOutline, { onClick: (e) => { onEdit(); e.stopPropagation(); }, style: { cursor: "pointer" }, "data-testid": "edit-icon" }) }) ] }); }; const StaticCellValue = ({ type, typeOptions, wrap, value, onEdit }) => { const { classes } = useStyles(); const canEdit = !!onEdit; if (type === "string") { return /* @__PURE__ */ jsx(OverflowText, { className: classes.cellPadding, wrap, value: value != null ? String(value) : value }); } else if (type === "link") { if (!value) return null; return /* @__PURE__ */ jsx(OverflowText, { className: classes.cellPadding, wrap, value: /* @__PURE__ */ jsx(Link, { size: "sm", href: value, className: classes.link, children: /* @__PURE__ */ jsx("span", { className: classes.linkSpan, children: value }) }) }); } else if (type === "number") { return /* @__PURE__ */ jsx(OverflowText, { className: classes.cellPadding, wrap, value }); } else if (type === "boolean") { return /* @__PURE__ */ jsx(CheckboxCell, { value }); } else if (type === "date") { let v; if (value != null) { const d = new Date(value); v = d.toLocaleDateString("en-us", { month: "short", day: "2-digit", year: "numeric" }); } return /* @__PURE__ */ jsx(OverflowText, { className: classes.cellPadding, wrap, value: v }); } else if (type === "datetime") { let v; if (value != null) { v = formatDatetime(value); } return /* @__PURE__ */ jsx(OverflowText, { className: classes.cellPadding, wrap, value: v }); } else if (type === "json") { return /* @__PURE__ */ jsx(Code, { language: "json", copy: !canEdit, theme: "light", style: { width: "100%", cursor: "default" }, radius: 0, children: formatJSON(value) }); } else if (type === "select") { if (!(typeOptions == null ? void 0 : typeOptions.selectData)) { throw new Error("Missing selectData in column.typeOptions"); } return /* @__PURE__ */ jsx(SelectComponent, { className: classes.cellPadding, value, data: typeOptions.selectData, clearable: false, size: "sm", readOnly: true, withinPortal: true, unstyled: true }); } else { assertNever(type); return null; } }; const textareaEditCompleteKeys = [["Enter", "Shift"], "Tab"]; const editCompleteKeys = ["Enter", "Tab"]; const editCancelKeys = ["Escape"]; const alwaysEditingCellTypes = ["boolean"]; const EditableCell = (props) => { const { value, type, typeOptions, onChange, onStopEditing, isDirty } = props; const { classes, cx } = useStyles(); const ref = useClickOutside(onStopEditing); let targetCompleteKeys = editCompleteKeys; let listenToWindow = true; switch (type) { case "string": case "json": case "link": targetCompleteKeys = textareaEditCompleteKeys; listenToWindow = false; break; case "boolean": targetCompleteKeys = ""; listenToWindow = false; break; } const editComplete = useKeyPress({ targetKeys: targetCompleteKeys, listenToWindow }); const editCancelKeyPressed = useKeyPress({ targetKeys: editCancelKeys, listenToWindow: true }); const [hover, setHover] = useState(false); useEffect(() => { if (editComplete.keyPressed) { onStopEditing(); } }, [type, editComplete.keyPressed, onStopEditing]); useEffect(() => { if (editCancelKeyPressed.keyPressed) { onStopEditing(true); } }, [editCancelKeyPressed.keyPressed, onStopEditing]); let cell; switch (type) { case "string": case "link": cell = /* @__PURE__ */ jsx(TextareaComponent, { value, onChange: (e) => onChange(e.target.value), onClick: (e) => e.stopPropagation(), "data-cy": "cell-text-input", size: "sm", variant: "unstyled", autoFocus: true, onFocus: (e) => { const val = e.target.value; e.target.value = ""; e.target.value = val; }, autosize: true, onKeyDown: (e) => { const isPressed = editComplete.downHandler(e); if (isPressed) { e.preventDefault(); } }, onKeyUp: editComplete.upHandler, classNames: { input: classes.textareaInput, wrapper: classes.textareaWrapper, root: classes.textareaRoot }, ref }); break; case "number": cell = /* @__PURE__ */ jsx(NumberInputComponent, { value, onChange: (v) => { if (v != null && (typeOptions == null ? void 0 : typeOptions.numberMin) != null) { v = Math.max(typeOptions.numberMin, v); } if (v != null && (typeOptions == null ? void 0 : typeOptions.numberMax) != null) { v = Math.min(typeOptions.numberMax, v); } onChange(v); }, onClick: (e) => e.stopPropagation(), autoFocus: true, ref, variant: "unstyled", size: "sm", className: classes.cellPadding }); break; case "boolean": cell = /* @__PURE__ */ jsx(CheckboxCell, { value, onChange, hoveringOnCell: hover }); break; case "date": cell = /* @__PURE__ */ jsx(DatePickerComponent, { value: value == null ? /* @__PURE__ */ new Date() : new Date(value), onChange, initiallyOpened: true, onDropdownClose: onStopEditing, clearable: false, closeCalendarOnChange: false, variant: "unstyled", size: "sm", withinPortal: true, autoFocus: true, className: classes.cellPadding }); break; case "datetime": cell = /* @__PURE__ */ jsx(DateTimePickerComponent, { value: value == null ? /* @__PURE__ */ new Date() : new Date(value), onChange, initiallyOpened: true, onDropdownClose: onStopEditing, clearable: false, variant: "unstyled", size: "sm", withinPortal: true, autoFocus: true, className: classes.cellPadding }); break; case "json": { cell = /* @__PURE__ */ jsx(EditableJSON, { onChange, onStopEditing, ref, value, onKeyDown: (e) => { const isPressed = editComplete.downHandler(e); if (isPressed) { e.preventDefault(); } }, onKeyUp: editComplete.upHandler }); break; } case "select": if (!(typeOptions == null ? void 0 : typeOptions.selectData)) { throw new Error("Missing selectData in column.typeOptions"); } cell = /* @__PURE__ */ jsx(SelectComponent, { value, data: typeOptions.selectData, initiallyOpened: true, clearable: false, size: "sm", onChange, ref, withinPortal: true, unstyled: true, classNames: { root: classes.cellPadding } }); break; default: assertNever(type); } return /* @__PURE__ */ jsx("div", { className: cx(classes.cell, { [classes.editingCell]: type !== "boolean" && type !== "json", // Only set dirty for boolean cells because it looks weird when editing cells // of other types. [classes.dirty]: isDirty && type === "boolean" }), onMouseEnter: () => setHover(true), onMouseLeave: () => setHover(false), "data-testid": `editable-cell${isDirty ? "-dirty" : ""}`, children: cell }); }; const EditableJSON = /* @__PURE__ */ React.forwardRef(({ value, onChange, onStopEditing: _, ...props }, ref) => { const [previousValue, setPreviousValue] = useState(""); useEffect(() => { if (previousValue != value) { setPreviousValue(""); } }, [value, setPreviousValue, previousValue]); let v = value; if (previousValue !== value) { v = formatJSON(value); } return ( // TODO: replace with Views component /* @__PURE__ */ jsx(JsonInput, { value: v, onChange: (v2) => { onChange(v2); setPreviousValue(v2); }, onClick: (e) => e.stopPropagation(), minRows: 8, autoFocus: true, ref, sx: { width: "100%" }, ...props }) ); }); EditableJSON.displayName = "JsonInput"; const CheckboxCell = ({ value, onChange, hoveringOnCell }) => { const theme = useMantineTheme(); const { classes } = useStyles(); return /* @__PURE__ */ jsx(CheckboxComponent, { "aria-label": "toggle", checked: value === true || value === "true", onChange: (checked) => onChange == null ? void 0 : onChange(checked, true), onClick: (e) => e.stopPropagation(), "data-cy": "cell-checkbox", size: "md", styles: { input: { backgroundColor: "white !important", border: hoveringOnCell && onChange ? void 0 : "none", borderColor: `${theme.colors.dark[0]} !important`, cursor: onChange ? void 0 : "default" }, icon: { color: `${theme.colors.primary[5]} !important` } }, className: classes.checkboxCellPadding }); }; function formatJSON(value) { let v = value; if (typeof value === "string") { try { v = JSON.stringify(JSON.parse(value), null, 2); } catch (e) { } } else if (value == null) { return ""; } else { try { v = JSON.stringify(value, null, 2); } catch (e) { } } return String(v); } const ISO_8601_FULL = /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?(([+-]\d\d:\d\d)|Z)?$/i; function getDefaultCellType(value, typeOptions) { if (typeof value === "boolean") { return "boolean"; } else if (typeof value === "number") { return "number"; } else if (value instanceof Date) { return "datetime"; } else if (value && typeof value === "object") { return "json"; } else if (typeof value === "string" && ISO_8601_FULL.test(value)) { return "datetime"; } else if (typeOptions == null ? void 0 : typeOptions.selectData) { return "select"; } return "string"; } const dateTimeSort = (rowA, rowB, columnID) => { const diff = new Date(rowA.values[columnID]).getTime() - new Date(rowB.values[columnID]).getTime(); if (!diff) return 0; return diff > 0 ? 1 : -1; }; export { Cell, dateTimeSort, getDefaultCellType }; //# sourceMappingURL=Cell.js.map