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/components/misc/save-button.tsx
Views: 687
1
import { CSSProperties, useEffect, useMemo, useRef, useState } from "react";
2
import { cloneDeep, debounce, isEqual } from "lodash";
3
import { Alert, Button, Space } from "antd";
4
import useIsMounted from "lib/hooks/mounted";
5
import Loading from "components/share/loading";
6
import api from "lib/api/post";
7
import { Icon } from "@cocalc/frontend/components/icon";
8
import { SCHEMA } from "@cocalc/util/schema";
9
import { keys } from "@cocalc/util/misc";
10
11
interface Props {
12
edited: any;
13
original: any;
14
setOriginal: Function;
15
table?: string;
16
style?: CSSProperties;
17
onSave?: Function; // if onSave is async then awaits and if there is an error shows that; if not, updates state to what was saved.
18
isValid?: (object) => boolean; // if given, only allow saving if edited != original and isValid(edited) is true.
19
debounce_ms?: number; // default is DEBOUNCE_MS
20
disabled?: boolean; // if given, overrides internaal logic.
21
}
22
23
const DEBOUNCE_MS = 1500;
24
25
export default function SaveButton({
26
disabled,
27
edited,
28
original,
29
setOriginal,
30
table,
31
style,
32
onSave,
33
isValid,
34
debounce_ms,
35
}: Props) {
36
if (debounce_ms == null) debounce_ms = DEBOUNCE_MS;
37
const [saving, setSaving] = useState<boolean>(false);
38
const [error, setError] = useState<string>("");
39
40
// Tricky hooks: We have to store the state in a ref as well so that
41
// we can use it in the save function, since that function
42
// is memoized and called from a debounced function.
43
const saveRef = useRef<any>({ edited, original, table });
44
saveRef.current = { edited, original, table };
45
46
const isMounted = useIsMounted();
47
48
const save = useMemo(() => {
49
return async () => {
50
const { edited, original, table } = saveRef.current;
51
52
let changes: boolean = false;
53
const e: any = {};
54
for (const field in edited) {
55
if (!isEqual(original[field], edited[field])) {
56
e[field] = cloneDeep(edited[field]);
57
changes = true;
58
}
59
}
60
if (!changes) {
61
// no changes to save.
62
return false;
63
}
64
65
for (const field of preserveFields(table)) {
66
e[field] = cloneDeep(edited[field]);
67
}
68
const query = { [table]: e };
69
if (isMounted.current) {
70
setSaving(true);
71
setError("");
72
}
73
let result;
74
try {
75
// Note -- we definitely do want to do the save
76
// itself, even if the component is already unmounted,
77
// so we don't loose changes.
78
result = await api("/user-query", { query });
79
} catch (err) {
80
if (!isMounted.current) return;
81
setError(err.message);
82
return false;
83
} finally {
84
if (isMounted.current) {
85
setSaving(false);
86
}
87
}
88
if (!isMounted.current) return;
89
if (result.error) {
90
setError(result.error);
91
} else {
92
setOriginal(cloneDeep(edited));
93
}
94
return true; // successful save
95
};
96
}, []);
97
98
function doSave() {
99
(async () => {
100
const e = cloneDeep(saveRef.current.edited);
101
if (table) {
102
const didSave = await save();
103
if (!isMounted.current) return;
104
if (didSave) {
105
await onSave?.(e);
106
}
107
return;
108
}
109
try {
110
await onSave?.(e);
111
if (!isMounted.current) return;
112
setOriginal(e);
113
setError("");
114
} catch (err) {
115
setError(err.toString());
116
}
117
})();
118
}
119
120
const doSaveDebounced = useMemo(
121
() => debounce(doSave, debounce_ms),
122
[onSave]
123
);
124
125
useEffect(() => {
126
doSaveDebounced();
127
return doSaveDebounced;
128
}, [edited]);
129
130
const same = isEqual(edited, original);
131
return (
132
<div style={style}>
133
<Button
134
type="primary"
135
disabled={
136
disabled ?? (saving || same || (isValid != null && !isValid(edited)))
137
}
138
onClick={doSave}
139
>
140
<Space>
141
<Icon name={"save"} />
142
{saving ? (
143
<Loading delay={250} before="Save">
144
Saving...
145
</Loading>
146
) : (
147
"Save"
148
)}
149
</Space>
150
</Button>
151
{!same && error && (
152
<Alert type="error" message={error} style={{ marginTop: "15px" }} />
153
)}
154
</div>
155
);
156
}
157
158
function preserveFields(table: string): string[] {
159
return keys(SCHEMA[table].user_query?.set?.required_fields ?? {});
160
}
161
162