Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restore points 2.0 #776

Merged
merged 21 commits into from
Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/addons/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ const AddonHooks = {
appStateReducer: () => {},
appStateStore: null,
blockly: null,
blocklyCallbacks: []
blocklyCallbacks: [],
disableRestorePoints: false
};

export default AddonHooks;
2 changes: 2 additions & 0 deletions src/components/gui/gui.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import TWUsernameModal from '../../containers/tw-username-modal.jsx';
import TWSettingsModal from '../../containers/tw-settings-modal.jsx';
import TWSecurityManager from '../../containers/tw-security-manager.jsx';
import TWCustomExtensionModal from '../../containers/tw-custom-extension-modal.jsx';
import TWRestorePointManager from '../../containers/tw-restore-point-manager.jsx';

import layout, {STAGE_SIZE_MODES} from '../../lib/layout-constants';
import {resolveStageSize} from '../../lib/screen-utils';
Expand Down Expand Up @@ -167,6 +168,7 @@ const GUIComponent = props => {
const alwaysEnabledModals = (
<React.Fragment>
<TWSecurityManager />
<TWRestorePointManager />
{usernameModalVisible && <TWUsernameModal />}
{settingsModalVisible && <TWSettingsModal />}
{customExtensionModalVisible && <TWCustomExtensionModal />}
Expand Down
29 changes: 15 additions & 14 deletions src/components/menu-bar/menu-bar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,9 @@ import MenuBarHOC from '../../containers/menu-bar-hoc.jsx';
import FramerateChanger from '../../containers/tw-framerate-changer.jsx';
import ChangeUsername from '../../containers/tw-change-username.jsx';
import CloudVariablesToggler from '../../containers/tw-cloud-toggler.jsx';
import TWRestorePointLoader from '../../containers/tw-restore-point-loader.jsx';
import TWSaveStatus from './tw-save-status.jsx';

import {openTipsLibrary, openSettingsModal} from '../../reducers/modals';
import {openTipsLibrary, openSettingsModal, openRestorePointModal} from '../../reducers/modals';
import {setPlayer} from '../../reducers/mode';
import {
autoUpdateProject,
Expand Down Expand Up @@ -205,6 +204,7 @@ class MenuBar extends React.Component {
'handleClickSave',
'handleClickSaveAsCopy',
'handleClickPackager',
'handleClickRestorePoints',
'handleClickSeeCommunity',
'handleClickShare',
'handleKeyPress',
Expand Down Expand Up @@ -251,6 +251,10 @@ class MenuBar extends React.Component {
this.props.onClickPackager();
this.props.onRequestCloseFile();
}
handleClickRestorePoints () {
this.props.onClickRestorePoints();
this.props.onRequestCloseFile();
}
handleClickSeeCommunity (waitForUpdate) {
if (this.props.shouldSaveBeforeTransition()) {
this.props.autoUpdateProject(); // save before transitioning to project page
Expand Down Expand Up @@ -644,18 +648,13 @@ class MenuBar extends React.Component {
</MenuSection>
)}
<MenuSection>
<TWRestorePointLoader>{(className, loadRestorePoint) => (
<MenuItem
className={className}
onClick={loadRestorePoint}
>
<FormattedMessage
defaultMessage="Load restore point"
description="Menu bar item for loading a restore point"
id="tw.menuBar.loadRestorePoint"
/>
</MenuItem>
)}</TWRestorePointLoader>
<MenuItem onClick={this.handleClickRestorePoints}>
<FormattedMessage
defaultMessage="Restore points"
description="Menu bar item to manage restore points"
id="tw.menuBar.restorePoints"
/>
</MenuItem>
</MenuSection>
</MenuBarMenu>
</div>
Expand Down Expand Up @@ -963,6 +962,7 @@ MenuBar.propTypes = {
onClickAddonSettings: PropTypes.func,
onClickTheme: PropTypes.func,
onClickPackager: PropTypes.func,
onClickRestorePoints: PropTypes.func,
onClickEdit: PropTypes.func,
onClickFile: PropTypes.func,
onClickLanguage: PropTypes.func,
Expand Down Expand Up @@ -1061,6 +1061,7 @@ const mapDispatchToProps = dispatch => ({
onClickRemix: () => dispatch(remixProject()),
onClickSave: () => dispatch(manualUpdateProject()),
onClickSaveAsCopy: () => dispatch(saveProjectAsCopy()),
onClickRestorePoints: () => dispatch(openRestorePointModal()),
onClickSettings: () => {
dispatch(openSettingsModal());
dispatch(closeEditMenu());
Expand Down
132 changes: 132 additions & 0 deletions src/components/tw-restore-point-modal/restore-point-modal.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
@import "../../css/colors.css";

.modal-content {
max-width: 550px;
margin-top: 50px;
}

.body {
background: $ui-white;
padding: 1.5rem 2.25rem;
max-height: calc(100vh - 150px);
overflow: auto;
}
[theme="dark"] .body {
color: $text-primary;
background: $ui-primary;
}

.extra-container,
.disabled,
.loading,
.error,
.empty,
.restore-point-container,
.legacy-transition {
margin: 1rem 0 0 0;
}

.extra-container {
display: flex;
justify-content: space-between;
align-items: center;
}
.total-size {

}
.button {
font: inherit;
color: inherit;
padding: 0.75rem 1rem;
border-radius: 0.25rem;
border: 1px solid $ui-black-transparent;
font-weight: 600;
font-size: 0.85rem;
color: $ui-white;
}
.button:disabled {
opacity: 0.8;
}
.delete-all-button {
margin-left: auto;
background-color: $data-primary;
}

.error-message {
font-family: monospace;
user-select: text;
}

.restore-point-container {
display: grid;
grid-template-columns: 1fr;
gap: 0.5rem;
}

.restore-point {
width: 100%;
cursor: pointer;
display: flex;
align-items: center;
border: 2px solid $ui-black-transparent;
padding: 0.5rem;
border-radius: 0.5rem;
gap: 0.5rem;
}
.restore-point:hover {
border-color: $motion-primary;
}
.restore-point-details {

}
.restore-point-thumbnail {
display: block;
border-radius: 0.25rem;
width: 100px;
height: 100%;
max-height: 100px;
}
.restore-point-title {
font-weight: bold;
}

.delete-button {
appearance: none;
background: none;
border: none;
display: flex;
align-items: center;
justify-content: center;
border-radius: 100%;
width: 2rem;
height: 2rem;
font-size: 2rem;
line-height: 1;
margin-left: auto;
}
.delete-button:hover {
background-color: $ui-black-transparent;
}

.disabled {
padding: 0.5rem;
border-radius: 0.5rem;
background-color: rgba(255, 0, 0, 0.18);
border: 2px solid rgba(255, 0, 0, 0.568);
font-weight: bold;
}

.legacy-transition {
display: flex;
justify-content: space-between;
padding: 0.5rem;
border-radius: 0.5rem;
background-color: rgba(128, 0, 128, 0.18);
border: 2px solid rgba(128, 0, 128, 0.568);
text-align: center;
font-weight: bold;
}
.load-legacy-button {
margin-left: 1rem;
background-color: $pen-primary;
}
160 changes: 160 additions & 0 deletions src/components/tw-restore-point-modal/restore-point-modal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import {defineMessages, FormattedMessage, intlShape, injectIntl} from 'react-intl';
import PropTypes from 'prop-types';
import React from 'react';
import Modal from '../../containers/modal.jsx';
import RestorePoint from './restore-point.jsx';
import styles from './restore-point-modal.css';
import classNames from 'classnames';
import {APP_NAME} from '../../lib/brand';
import {formatBytes} from '../../lib/tw-bytes-utils';

const messages = defineMessages({
title: {
defaultMessage: 'Restore Points',
description: 'Title of restore point management modal',
id: 'tw.restorePoints.title'
}
});

const RestorePointModal = props => (
<Modal
className={styles.modalContent}
onRequestClose={props.onClose}
contentLabel={props.intl.formatMessage(messages.title)}
id="restorePointModal"
>
<div className={styles.body}>
<p>
<FormattedMessage
// eslint-disable-next-line max-len
defaultMessage="{APP_NAME} periodically creates restore points to help recover your project if you forget to save. This is intended as a last resort for recovery. Your computer may silently delete these restore points at any time. Do not rely on this feature."
id="tw.restorePoints.description"
values={{
APP_NAME: APP_NAME
}}
/>
</p>

{props.disabled && (
<p className={styles.disabled}>
<FormattedMessage
// eslint-disable-next-line max-len
defaultMessage="Disable the &quot;Disable restore points&quot; addon to re-enable restore point creation."
// eslint-disable-next-line max-len
description="Message that appears in restore point manager when the user has disabled restore points. Note that the name of the addon in the addon settings is currently hardcoded as English."
id="tw.restorePoints.disabled"
/>
</p>
)}

{props.error ? (
<div className={styles.error}>
<p>
<FormattedMessage
defaultMessage="Restore points are not available due to an error:"
// eslint-disable-next-line max-len
description="Error message in restore point manager when the list of restore points cannot be loaded. Followed by an error message."
id="tw.restorePoints.error"
values={{
error: props.error
}}
/>
</p>
<p className={styles.errorMessage}>
{props.error}
</p>
</div>
) : props.isLoading ? (
<div className={styles.loading}>
<FormattedMessage
defaultMessage="Loading..."
description="Loading message in restore point manager"
id="tw.restorePoints.loading"
/>
</div>
) : props.restorePoints.length === 0 ? (
<div>
<div className={styles.empty}>
<FormattedMessage
defaultMessage="No restore points found."
description="Message that appears when no restore points exist yet"
id="tw.restorePoints.empty"
/>
</div>
</div>
) : (
<div>
<div className={styles.restorePointContainer}>
{props.restorePoints.map(restorePoint => (
<RestorePoint
key={restorePoint.id}
onClickDelete={props.onClickDelete}
onClickLoad={props.onClickLoad}
{...restorePoint}
/>
))}
</div>

<div className={styles.extraContainer}>
<div className={styles.totalSize}>
<FormattedMessage
defaultMessage="Estimated storage used: {size}"
description="Part of restore point modal describing amount of disk space used"
id="tw.restorePoints.size"
values={{
size: formatBytes(props.totalSize)
}}
/>
</div>

<button
onClick={props.onClickDeleteAll}
className={classNames(styles.button, styles.deleteAllButton)}
disabled={props.isLoading}
>
<FormattedMessage
defaultMessage="Delete All"
description="Button to delete all restore points"
id="tw.restorePoints.deleteAll"
/>
</button>
</div>
</div>
)}

{!props.isLoading && (
<div className={styles.legacyTransition}>
{/* This is going away within a few days */}
{/* No reason to bother translating */}
<span>
{/* eslint-disable-next-line max-len */}
{'We just rewrote restore points from the ground up. If you can\'t find your project in the list, try loading the old restore point:'}
</span>
<button
className={classNames(styles.button, styles.loadLegacyButton)}
onClick={props.onClickLoadLegacy}
>
{'Load'}
</button>
</div>
)}
</div>
</Modal>
);

RestorePointModal.propTypes = {
intl: intlShape,
onClose: PropTypes.func.isRequired,
onClickCreate: PropTypes.func.isRequired,
onClickDelete: PropTypes.func.isRequired,
onClickDeleteAll: PropTypes.func.isRequired,
onClickLoad: PropTypes.func.isRequired,
onClickLoadLegacy: PropTypes.func.isRequired,
disabled: PropTypes.bool.isRequired,
isLoading: PropTypes.bool.isRequired,
totalSize: PropTypes.number.isRequired,
restorePoints: PropTypes.arrayOf(PropTypes.shape({})),
error: PropTypes.string
};

export default injectIntl(RestorePointModal);
Loading
Loading