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