CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

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