Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/next/lib/hooks/edit-table.tsx
Views: 687
/*1* This file is part of CoCalc: Copyright © 2021 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { Space } from "antd";6import { cloneDeep, get, keys, set } from "lodash";7import {8CSSProperties,9ReactNode,10useEffect,11useMemo,12useRef,13useState,14} from "react";1516import { Icon, IconName } from "@cocalc/frontend/components/icon";17import { capitalize } from "@cocalc/util/misc";18import { SCHEMA } from "@cocalc/util/schema";19import { Paragraph, Title } from "components/misc";20import Checkbox from "components/misc/checkbox";21import IntegerSlider from "components/misc/integer-slider";22import SaveButton from "components/misc/save-button";23import SelectWithDefault from "components/misc/select-with-default";24import useDatabase from "lib/hooks/database";2526/*27WARNING: the code below is some pretty complicated use of React hooks,28in order to make the API it exports simple and easy to use with minimal29replication of code.3031For example, we have to inroduce editRef and a complicated setEdited32below so that users don't have to explicitly pass setEdited into33components like EditNumber, which would be annoying, tedious and silly.34The code is complicated due to how hooks work, when components get created35and updated, etc. Say to yourself: "I still know closure fu."36*/3738interface Options {39onSave?: Function;40noSave?: boolean;41}4243interface HeadingProps {44path?: string;45title?: string;46desc?: ReactNode;47icon?: IconName;48}4950interface EditBooleanProps {51path: string;52title?: string;53desc?: ReactNode;54label?: ReactNode;55icon?: IconName;56}5758export default function useEditTable<T extends object>(59query: object,60options?: Options,61) {62const { loading, value } = useDatabase(query);63const [original, setOriginal] = useState<T | undefined>(undefined);64const [edited, setEdited0] = useState<T | undefined>(undefined);65const [counter, setCounter] = useState<number>(0);66const editedRef = useRef<T | undefined>(edited);67editedRef.current = edited;6869function setEdited(update, path?: string) {70if (!path) {71// usual setEdited72setEdited0(update);73// force a full update74setCounter(counter + 1);75return;76}77if (editedRef.current == null) return;78// just edit part of object.79set(editedRef.current, path, update);80setEdited0({ ...editedRef.current } as T);81}8283useEffect(() => {84if (!loading) {85setOriginal(cloneDeep(value[keys(value)[0]]));86setEdited(value[keys(value)[0]]);87}88}, [loading]);8990function Save() {91if (edited == null || original == null || options?.noSave) return null;92return (93<div>94<SaveButton95style={{ marginBottom: "10px" }}96edited={edited}97original={original}98setOriginal={setOriginal}99table={keys(query)[0]}100onSave={options?.onSave}101/>102</div>103);104}105106function Heading(props: HeadingProps) {107const { path, title, icon, desc } = props;108return (109<>110<Title level={3}>111{icon && <Icon name={icon} style={{ marginRight: "10px" }} />}112{getTitle(path, title)}113</Title>114{desc && <Paragraph>{desc}</Paragraph>}115</>116);117}118119function EditBoolean(props: EditBooleanProps) {120const { path, title, desc, label, icon } = props;121return (122<Space direction="vertical" style={{ marginTop: "15px" }}>123<Heading path={path} title={title} icon={icon} desc={desc} />124<Checkbox125defaultValue={get(126SCHEMA[keys(query)[0]].user_query?.get?.fields,127path,128)}129checked={get(edited, path)}130onChange={(checked) => {131setEdited(checked, path);132}}133>134{getLabel(path, title, label)}135</Checkbox>136</Space>137);138}139140// It's very important EditNumber isn't recreated once141// edited and original are both not null, since the text142// field would then lose focus. Also, it has to not be143// a controlled component, since otherwise edited has to144// be passed in externally, which is very awkward.145const EditNumber = useMemo(() => {146if (edited == null || original == null) return () => null;147return ({148path,149title,150desc,151units,152min,153max,154icon,155}: {156path: string;157title?: string;158desc?: ReactNode;159units?: string;160min: number;161max: number;162icon?: IconName;163}) => (164<Space direction="vertical" style={{ width: "100%" }}>165<Heading path={path} title={title} icon={icon} desc={desc} />166<IntegerSlider167defaultValue={get(168SCHEMA[keys(query)[0]].user_query?.get?.fields,169path,170)}171initialValue={get(edited, path)}172onChange={(value) => {173setEdited(value, path);174}}175min={min}176max={max}177units={units}178/>179</Space>180);181}, [edited == null, original == null, counter]);182183const EditSelect = useMemo(() => {184if (edited == null || original == null) return () => null;185return ({186path,187title,188desc,189icon,190options,191style,192defaultValue,193}: {194path: string;195title?: string;196desc?: ReactNode;197icon?: IconName;198options: { [value: string]: ReactNode } | string[];199style?: CSSProperties;200defaultValue?: string;201}) => (202<Space direction="vertical">203<Heading path={path} title={title} icon={icon} desc={desc} />204<SelectWithDefault205style={style}206defaultValue={207defaultValue ??208get(SCHEMA[keys(query)[0]].user_query?.get?.fields, path)209}210initialValue={get(edited, path)}211onChange={(value) => {212setEdited(value, path);213}}214options={options}215/>216</Space>217);218}, [edited == null, original == null, counter]);219220return {221edited,222original,223Save,224setEdited,225EditBoolean,226EditNumber,227EditSelect,228Heading,229};230}231232function getTitle(path?: string, title?: string): string {233if (title) return title;234if (!path) return "";235const v = path.split(".");236return v[v.length - 1].split("_").map(capitalize).join(" ");237}238239function getLabel(path: string, title?: string, label?: ReactNode): ReactNode {240return label ?? capitalize(getTitle(path, title).toLowerCase());241}242243244