Skip to content

Commit 89eabee

Browse files
committed
automatically start a project if there is a file open with changes that are failing to save
- whether or not this is a good idea is unclear and there is controversy - i always believe preventing user data loss is of utmost importance, which is why we have snapshots, timetravel, etc. - hence this feature
1 parent 11e90f5 commit 89eabee

File tree

2 files changed

+104
-87
lines changed

2 files changed

+104
-87
lines changed

src/packages/frontend/components/uncommited-changes.tsx

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,17 @@
33
* License: MS-RSL – see LICENSE.md for details
44
*/
55

6-
// Component that shows a warning message if has_uncommitted_changes is true for more than a few seconds.
6+
/*
7+
Component that shows a warning message if has_uncommitted_changes is true for more than a few seconds.
8+
9+
In case the project-id is known via file context and the project is not running, this *also* will
10+
autoamtically start the project running. This is to **avoid data loss**, since there is no way
11+
to save what is not getting saved without starting the project.
12+
*/
713

8-
import { useState, useEffect, memo } from "react";
14+
import { useState, useEffect } from "react";
15+
import { useFileContext } from "@cocalc/frontend/lib/file-context";
16+
import { redux } from "@cocalc/frontend/app-framework";
917

1018
interface Props {
1119
has_uncommitted_changes?: boolean;
@@ -31,47 +39,59 @@ const STYLE = {
3139
*
3240
* Does not work with changes to `delay_ms`
3341
*/
34-
const UncommittedChangesFC = (props: Props) => {
35-
const {
36-
has_uncommitted_changes,
37-
show_uncommitted_changes,
38-
set_show_uncommitted_changes,
39-
delay_ms = 5000,
40-
} = props;
42+
export function UncommittedChanges({
43+
has_uncommitted_changes,
44+
show_uncommitted_changes,
45+
set_show_uncommitted_changes,
46+
delay_ms = 5000,
47+
}: Props) {
48+
const { project_id } = useFileContext();
4149
const init = has_uncommitted_changes && (show_uncommitted_changes ?? false);
42-
const [show_error, set_error] = useState(init);
50+
const [showError, setShowError0] = useState<boolean>(!!init);
51+
52+
const setShowError = (val) => {
53+
setShowError0(val);
54+
if (project_id != null && val && !showError) {
55+
// changed from no error to showing an error
56+
if (
57+
redux
58+
.getStore("projects")
59+
?.getIn(["project_map", project_id, "state", "state"]) != "running"
60+
) {
61+
redux.getActions("projects").start_project(project_id);
62+
}
63+
}
64+
};
4365

4466
// A new interval is created iff has_uncommitted_changes or delay_ms change
4567
// So error is only set to true when the prop doesn't change for ~delay_ms time
4668
useEffect(() => {
4769
if (!init) {
48-
set_error(init);
70+
setShowError(!!init);
4971
}
50-
const interval_id = setInterval(() => {
72+
const intervalId = setInterval(() => {
5173
if (
5274
show_uncommitted_changes != null &&
5375
set_show_uncommitted_changes != null
5476
) {
5577
const next = has_uncommitted_changes;
5678
set_show_uncommitted_changes(next);
57-
set_error(next);
79+
setShowError(!!next);
5880
} else {
5981
if (has_uncommitted_changes) {
60-
set_error(true);
82+
setShowError(true);
6183
}
6284
}
6385
}, delay_ms + 10);
6486

65-
return function cleanup() {
66-
clearInterval(interval_id);
87+
return () => {
88+
clearInterval(intervalId);
6789
};
6890
}, [has_uncommitted_changes, delay_ms, show_uncommitted_changes, init]);
6991

70-
if (show_error) {
92+
if (showError) {
7193
return <span style={STYLE}>NOT saved!</span>;
7294
} else {
7395
return null;
7496
}
75-
};
76-
77-
export const UncommittedChanges = memo(UncommittedChangesFC);
97+
}

src/packages/frontend/frame-editors/frame-tree/save-button.tsx

Lines changed: 64 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import type { ButtonProps } from "antd";
77
import { Button } from "antd";
8-
import { CSSProperties, FC, memo, useMemo } from "react";
8+
import { CSSProperties, useMemo } from "react";
99
import { useIntl } from "react-intl";
1010

1111
import {
@@ -30,75 +30,72 @@ interface Props {
3030
type?: "default"; // only used to turn off color in case of dark mode right now
3131
}
3232

33-
export const SaveButton: FC<Props> = memo(
34-
({
35-
has_unsaved_changes,
36-
has_uncommitted_changes,
37-
read_only,
38-
is_public,
39-
is_saving,
40-
no_labels,
41-
size,
42-
onClick,
43-
show_uncommitted_changes,
44-
set_show_uncommitted_changes,
45-
style,
46-
type,
47-
}: Props) => {
48-
const intl = useIntl();
33+
export function SaveButton({
34+
has_unsaved_changes,
35+
has_uncommitted_changes,
36+
read_only,
37+
is_public,
38+
is_saving,
39+
no_labels,
40+
size,
41+
onClick,
42+
show_uncommitted_changes,
43+
set_show_uncommitted_changes,
44+
style,
45+
type,
46+
}: Props) {
47+
const intl = useIntl();
4948

50-
const label = useMemo(() => {
51-
if (!no_labels) {
52-
return intl.formatMessage(
53-
labels.frame_editors_title_bar_save_label,
54-
{ type: is_public ? "is_public" : read_only ? "read_only" : "save" },
55-
);
56-
} else {
57-
return null;
58-
}
59-
}, [no_labels, is_public, read_only]);
49+
const label = useMemo(() => {
50+
if (!no_labels) {
51+
return intl.formatMessage(labels.frame_editors_title_bar_save_label, {
52+
type: is_public ? "is_public" : read_only ? "read_only" : "save",
53+
});
54+
} else {
55+
return null;
56+
}
57+
}, [no_labels, is_public, read_only]);
6058

61-
const disabled = useMemo(
62-
() => !has_unsaved_changes || !!read_only || !!is_public,
63-
[has_unsaved_changes, read_only, is_public],
64-
);
59+
const disabled = useMemo(
60+
() => !has_unsaved_changes || !!read_only || !!is_public,
61+
[has_unsaved_changes, read_only, is_public],
62+
);
6563

66-
const icon = useMemo(
67-
() => (is_saving ? "arrow-circle-o-left" : "save"),
68-
[is_saving],
69-
);
64+
const icon = useMemo(
65+
() => (is_saving ? "arrow-circle-o-left" : "save"),
66+
[is_saving],
67+
);
7068

71-
function renderLabel() {
72-
if (!no_labels && label) {
73-
return <VisibleMDLG>{` ${label}`}</VisibleMDLG>;
74-
}
69+
function renderLabel() {
70+
if (!no_labels && label) {
71+
return <VisibleMDLG>{` ${label}`}</VisibleMDLG>;
7572
}
73+
}
7674

77-
// The funny style in the icon below is because the width changes
78-
// slightly depending on which icon we are showing.
79-
// whiteSpace:"nowrap" due to https://github.com/sagemathinc/cocalc/issues/4434
80-
return (
81-
<Button
82-
size={size}
83-
disabled={disabled}
84-
onClick={onClick}
85-
style={{
86-
...(type == "default"
87-
? undefined
88-
: { background: "#5cb85c", color: "#333" }),
89-
opacity: disabled ? 0.65 : undefined,
90-
whiteSpace: "nowrap",
91-
...style,
92-
}}
93-
>
94-
<Icon name={icon} style={{ display: "inline-block" }} />
95-
{renderLabel()}
96-
<UncommittedChanges
97-
has_uncommitted_changes={has_uncommitted_changes}
98-
show_uncommitted_changes={show_uncommitted_changes}
99-
set_show_uncommitted_changes={set_show_uncommitted_changes}
100-
/>
101-
</Button>
102-
);
103-
},
104-
);
75+
// The funny style in the icon below is because the width changes
76+
// slightly depending on which icon we are showing.
77+
// whiteSpace:"nowrap" due to https://github.com/sagemathinc/cocalc/issues/4434
78+
return (
79+
<Button
80+
size={size}
81+
disabled={disabled}
82+
onClick={onClick}
83+
style={{
84+
...(type == "default"
85+
? undefined
86+
: { background: "#5cb85c", color: "#333" }),
87+
opacity: disabled ? 0.65 : undefined,
88+
whiteSpace: "nowrap",
89+
...style,
90+
}}
91+
>
92+
<Icon name={icon} style={{ display: "inline-block" }} />
93+
{renderLabel()}
94+
<UncommittedChanges
95+
has_uncommitted_changes={has_uncommitted_changes}
96+
show_uncommitted_changes={show_uncommitted_changes}
97+
set_show_uncommitted_changes={set_show_uncommitted_changes}
98+
/>
99+
</Button>
100+
);
101+
}

0 commit comments

Comments
 (0)