@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
JavaScript
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