From a8dd014556d13efc3ec5231114734db787122841 Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Fri, 27 Feb 2026 11:21:00 +0200 Subject: [PATCH 01/13] chore: initial changes --- .../scratch-gui/src/components/gui/gui.jsx | 2 + .../src/components/menu-bar/menu-bar.jsx | 57 ++++++++++++++++++- .../src/components/menu-bar/save-status.jsx | 10 ++-- .../scratch-gui/src/lib/project-saver-hoc.jsx | 38 ++++++++----- 4 files changed, 86 insertions(+), 21 deletions(-) diff --git a/packages/scratch-gui/src/components/gui/gui.jsx b/packages/scratch-gui/src/components/gui/gui.jsx index 0452e9ba4cf..2270917bd2d 100644 --- a/packages/scratch-gui/src/components/gui/gui.jsx +++ b/packages/scratch-gui/src/components/gui/gui.jsx @@ -348,6 +348,7 @@ const GUIComponent = props => { canManageFiles={canManageFiles} canRemix={canRemix} canSave={canSave} + canUpdateThumbnail={userOwnsProject && manuallySaveThumbnails && !isPlayerOnly} canShare={canShare} className={styles.menuBarPosition} enableCommunity={enableCommunity} @@ -368,6 +369,7 @@ const GUIComponent = props => { onShare={onShare} onStartSelectingFileUpload={onStartSelectingFileUpload} onToggleLoginOpen={onToggleLoginOpen} + onUpdateProjectThumbnail={onUpdateProjectThumbnail} userOwnsProject={userOwnsProject} username={username} accountMenuOptions={accountMenuOptions} diff --git a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx index 79b865abb47..d5a09bf4455 100644 --- a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx +++ b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx @@ -30,6 +30,10 @@ import TurboMode from '../../containers/turbo-mode.jsx'; import MenuBarHOC from '../../containers/menu-bar-hoc.jsx'; import SettingsMenu from './settings-menu.jsx'; + +import {GUIStoragePropType} from '../../gui-config'; +import {storeProjectThumbnail} from '../../lib/store-project-thumbnail'; +import dataURItoBlob from '../../lib/data-uri-to-blob'; import {openTipsLibrary, openDebugModal} from '../../reducers/modals'; import {setPlayer} from '../../reducers/mode'; import { @@ -189,6 +193,7 @@ class MenuBar extends React.Component { 'handleClickRemix', 'handleClickSave', 'handleClickSaveAsCopy', + 'handleClickUpdateThumbnail', 'handleClickSeeCommunity', 'handleClickShare', 'handleSetMode', @@ -231,6 +236,26 @@ class MenuBar extends React.Component { this.props.onClickSaveAsCopy(); this.props.onRequestCloseFile(); } + handleClickUpdateThumbnail () { + const projectId = this.props.projectId?.toString(); + console.log(`Project saved with id ${projectId}`); + console.log(this.props.onUpdateProjectThumbnail); + console.log(this.props.vm); + if (this.props.storage?.internalApiHost) { + console.log('Storage host:', this.props.storage.internalApiHost); + } else if (this.props.storage) { + console.log('Storage object keys:', Object.keys(this.props.storage)); + } + if (!this.props.onUpdateProjectThumbnail) return; + storeProjectThumbnail(this.props.vm, dataURI => { + console.log('Updating thumbnail...'); + this.props.onUpdateProjectThumbnail( + projectId, + dataURItoBlob(dataURI) + ); + console.log('After dispatch'); + }); + } handleClickSeeCommunity (waitForUpdate) { if (this.props.shouldSaveBeforeTransition()) { this.props.autoUpdateProject(); // save before transitioning to project page @@ -385,6 +410,13 @@ class MenuBar extends React.Component { }; } render () { + const updateThumbnailMessage = ( + + ); const saveNowMessage = ( - {(this.props.canSave || this.props.canCreateCopy || this.props.canRemix) && ( + {(this.props.canSave || this.props.canCreateCopy || + this.props.canRemix || this.props.canUpdateThumbnail) && ( {this.props.canSave && ( @@ -513,6 +546,11 @@ class MenuBar extends React.Component { {remixMessage} )} + {this.props.canUpdateThumbnail && ( + + {updateThumbnailMessage} + + )} )} @@ -920,6 +958,7 @@ MenuBar.propTypes = { canManageFiles: PropTypes.bool, canRemix: PropTypes.bool, canSave: PropTypes.bool, + canUpdateThumbnail: PropTypes.bool, canShare: PropTypes.bool, className: PropTypes.string, confirmReadyToReplaceProject: PropTypes.func, @@ -943,6 +982,7 @@ MenuBar.propTypes = { mode220022BC: PropTypes.bool, modeMenuOpen: PropTypes.bool, modeNow: PropTypes.bool, + projectId: PropTypes.number.isRequired, onClickAbout: PropTypes.oneOfType([ PropTypes.func, // button mode: call this callback when the About button is clicked PropTypes.arrayOf( // menu mode: list of items in the About menu @@ -981,6 +1021,7 @@ MenuBar.propTypes = { onShare: PropTypes.func, onStartSelectingFileUpload: PropTypes.func, onToggleLoginOpen: PropTypes.func, + onUpdateProjectThumbnail: PropTypes.func, platform: PropTypes.oneOf(Object.keys(PLATFORM)), projectTitle: PropTypes.string, renderLogin: PropTypes.func, @@ -993,7 +1034,10 @@ MenuBar.propTypes = { accountMenuOptions: AccountMenuOptionsPropTypes, - vm: PropTypes.instanceOf(VM).isRequired + vm: PropTypes.instanceOf(VM).isRequired, + + + storage: GUIStoragePropType, }; MenuBar.defaultProps = { @@ -1006,6 +1050,7 @@ const mapStateToProps = (state, ownProps) => { const user = state.session && state.session.session && state.session.session.user; const permissions = state.session && state.session.permissions; const sessionExists = state.session && typeof state.session.session !== 'undefined'; + const storage = state.scratchGui.config.storage; return { aboutMenuOpen: aboutMenuOpen(state), @@ -1030,6 +1075,10 @@ const mapStateToProps = (state, ownProps) => { mode1990: isTimeTravel1990(state), mode2020: isTimeTravel2020(state), modeNow: isTimeTravelNow(state), + projectId: state.scratchGui.projectState.projectId, + onUpdateProjectThumbnail: + ownProps.onUpdateProjectThumbnail ?? + storage.saveProjectThumbnail?.bind(storage), platform: state.scratchGui.platform.platform, @@ -1050,7 +1099,9 @@ const mapStateToProps = (state, ownProps) => { myClassesUrl: permissions?.educator ? '/educators/classes/' : null, myClassUrl: user && permissions?.student ? `/classes/${user.classroomId}/` : null, accountSettingsUrl: '/accounts/settings/' - } + }, + + storage }; }; diff --git a/packages/scratch-gui/src/components/menu-bar/save-status.jsx b/packages/scratch-gui/src/components/menu-bar/save-status.jsx index 9f83f6274de..68c3e149d58 100644 --- a/packages/scratch-gui/src/components/menu-bar/save-status.jsx +++ b/packages/scratch-gui/src/components/menu-bar/save-status.jsx @@ -23,7 +23,8 @@ import styles from './save-status.css'; const SaveStatus = ({ alertsList, projectChanged, - onClickSave + onClickSave, + formattedMessage }) => ( filterInlineAlerts(alertsList).length > 0 ? ( @@ -32,18 +33,19 @@ const SaveStatus = ({ className={styles.saveNow} onClick={onClickSave} > - + />} )); SaveStatus.propTypes = { alertsList: PropTypes.arrayOf(PropTypes.object), onClickSave: PropTypes.func, - projectChanged: PropTypes.bool + projectChanged: PropTypes.bool, + formattedMessage: PropTypes.string }; const mapStateToProps = state => ({ diff --git a/packages/scratch-gui/src/lib/project-saver-hoc.jsx b/packages/scratch-gui/src/lib/project-saver-hoc.jsx index a753b4291ea..b224cda7426 100644 --- a/packages/scratch-gui/src/lib/project-saver-hoc.jsx +++ b/packages/scratch-gui/src/lib/project-saver-hoc.jsx @@ -230,9 +230,8 @@ const ProjectSaverHOC = function (WrappedComponent) { * @param {number|string|undefined} projectId - defined value will PUT/update; undefined/null will POST/create * @returns {Promise} - resolves with json object containing project's existing or new id * @param {?object} requestParams - object of params to add to request body - * @param {?object} options - additional options for the store operation */ - storeProject (projectId, requestParams, options) { + storeProject (projectId, requestParams) { requestParams = requestParams || {}; this.clearAutoSaveTimeout(); // Serialize VM state now before embarking on @@ -268,18 +267,9 @@ const ProjectSaverHOC = function (WrappedComponent) { .then(() => saveProject(projectId, savedVMState, requestParams)) .then(response => { this.props.onSetProjectUnchanged(); - const id = response.id.toString(); - if (this.props.onUpdateProjectThumbnail && id && ( - !this.props.manuallySaveThumbnails || - // Always save thumbnail on project creation - options?.isCreatingProject)) { - storeProjectThumbnail(this.props.vm, dataURI => { - this.props.onUpdateProjectThumbnail( - id, - dataURItoBlob(dataURI) - ); - }); - } + // Thumbnails are now manual only + // Do not update thumbnail on save + this.reportTelemetryEvent('projectDidSave'); return response; }) @@ -289,6 +279,26 @@ const ProjectSaverHOC = function (WrappedComponent) { }); } + updateThumbnail (projectId, requestParams, options) { + try { + if (this.props.onUpdateProjectThumbnail && projectId && ( + !this.props.manuallySaveThumbnails || + // Always save thumbnail on project creation + options?.isCreatingProject)) { + storeProjectThumbnail(this.props.vm, dataURI => { + this.props.onUpdateProjectThumbnail( + projectId, + dataURItoBlob(dataURI), + requestParams + ); + }); + } + } catch (err) { + log.error(err); + throw err; + } + } + /** * Report a telemetry event. * @param {string} event - one of `projectWasCreated`, `projectDidLoad`, `projectDidSave`, `projectWasUploaded` From 45c293530605025af69ffafac7eeea721b9b7e24 Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Thu, 12 Mar 2026 15:56:16 +0200 Subject: [PATCH 02/13] chore: moved button in editor and added modal --- .../src/components/button/button.jsx | 5 +- .../confirmation-prompt.css | 66 +++++ .../confirmation-prompt.jsx | 240 ++++++++++++++++++ .../confirmation-prompt/icon--arrow-down.svg | 3 + .../confirmation-prompt/icon--arrow-left.svg | 3 + .../confirmation-prompt/icon--arrow-right.svg | 3 + .../confirmation-prompt/icon--arrow-up.svg | 13 + .../scratch-gui/src/components/gui/gui.jsx | 8 +- .../src/components/menu-bar/menu-bar.jsx | 57 +---- .../stage-header/icon--thumbnail.svg | 4 + .../components/stage-header/stage-header.css | 11 +- .../components/stage-header/stage-header.jsx | 92 +++++-- .../stage-wrapper/stage-wrapper.jsx | 3 + .../scratch-gui/src/lib/project-saver-hoc.jsx | 38 +-- 14 files changed, 433 insertions(+), 113 deletions(-) create mode 100644 packages/scratch-gui/src/components/confirmation-prompt/confirmation-prompt.css create mode 100644 packages/scratch-gui/src/components/confirmation-prompt/confirmation-prompt.jsx create mode 100644 packages/scratch-gui/src/components/confirmation-prompt/icon--arrow-down.svg create mode 100644 packages/scratch-gui/src/components/confirmation-prompt/icon--arrow-left.svg create mode 100644 packages/scratch-gui/src/components/confirmation-prompt/icon--arrow-right.svg create mode 100644 packages/scratch-gui/src/components/confirmation-prompt/icon--arrow-up.svg create mode 100644 packages/scratch-gui/src/components/stage-header/icon--thumbnail.svg diff --git a/packages/scratch-gui/src/components/button/button.jsx b/packages/scratch-gui/src/components/button/button.jsx index eb4a5bdd4e1..447f2ff8c0f 100644 --- a/packages/scratch-gui/src/components/button/button.jsx +++ b/packages/scratch-gui/src/components/button/button.jsx @@ -11,6 +11,7 @@ const ButtonComponent = ({ iconSrc, onClick, children, + componentRef, ...props }) => { @@ -33,6 +34,7 @@ const ButtonComponent = ({ className )} onClick={onClick} + ref={componentRef} {...props} > {icon} @@ -47,7 +49,8 @@ ButtonComponent.propTypes = { disabled: PropTypes.bool, iconClassName: PropTypes.string, iconSrc: PropTypes.string, - onClick: PropTypes.func + onClick: PropTypes.func, + componentRef: PropTypes.func }; export default ButtonComponent; diff --git a/packages/scratch-gui/src/components/confirmation-prompt/confirmation-prompt.css b/packages/scratch-gui/src/components/confirmation-prompt/confirmation-prompt.css new file mode 100644 index 00000000000..30d9cb69a1c --- /dev/null +++ b/packages/scratch-gui/src/components/confirmation-prompt/confirmation-prompt.css @@ -0,0 +1,66 @@ +@import "../../css/colors.css"; +@import "../../css/units.css"; + +.modal-container { + display: flex; + width: 200px; + padding: 12px; + flex-direction: column; + align-items: flex-start; + border-radius: 8px; + background: #855CD6; + gap: 15px; + box-sizing: border-box; +} + +.label { + align-self: stretch; + color: var(--White-White, #FFF); + text-align: center; + font-family: "Helvetica Neue"; + font-size: 16px; + font-style: normal; + font-weight: 700; + line-height: 24px; +} + +.button-row { + font-weight: bolder; + display: flex; + gap: 0.5rem; + width: 100%; +} + +.button-row button { + all: unset; + display: flex; + padding: 8px 16px; + justify-content: center; + align-items: center; + gap: 8px; + flex: 1 0 0; + border-radius: 40px; + background: inherit; + cursor: pointer; +} + +.button-row button span { + color: var(--white-white-100, #FFF); + font-family: "Helvetica Neue"; + font-size: 12px; + font-style: normal; + font-weight: 700; + line-height: 16px; +} + +.button-row button.confirm-button { + background: var(--White-White, #FFF); +} + +.button-row button.confirm-button span { + color: var(--Looks-Purple-2, #855CD6); +} + +.button-row button.cancel-button { + border: 1px solid var(--white-white-100, #FFF); +} diff --git a/packages/scratch-gui/src/components/confirmation-prompt/confirmation-prompt.jsx b/packages/scratch-gui/src/components/confirmation-prompt/confirmation-prompt.jsx new file mode 100644 index 00000000000..ce583a982c5 --- /dev/null +++ b/packages/scratch-gui/src/components/confirmation-prompt/confirmation-prompt.jsx @@ -0,0 +1,240 @@ +import React, {useRef, useCallback} from 'react'; +import PropTypes from 'prop-types'; +import ReactModal from 'react-modal'; +import {defineMessages, FormattedMessage, useIntl} from 'react-intl'; + +import Box from '../box/box.jsx'; + +import arrowLeftIcon from './icon--arrow-left.svg'; +import arrowRightIcon from './icon--arrow-right.svg'; +import arrowDownIcon from './icon--arrow-down.svg'; +import arrowUpIcon from './icon--arrow-up.svg'; + +import styles from './confirmation-prompt.css'; + +const messages = defineMessages({ + defaultConfirmLabel: { + defaultMessage: 'yes', + description: 'Label for confirm button in confirmation prompt', + id: 'gui.confirmationPrompt.confirm' + }, + defaultCancelLabel: { + defaultMessage: 'no', + description: 'Label for cancel button in confirmation prompt', + id: 'gui.confirmationPrompt.cancel' + } +}); + +const modalWidth = 200; +const spaceForArrow = 16; +const arrowOffsetFromEnd = 7; +const arrowLongSide = 29; +const arrowShortSide = 13; + +const calculateModalPosition = (relativeElemRef, modalRef, modalPosition) => { + const arrowHeight = (modalPosition === 'left' || modalPosition === 'right') ? + arrowLongSide : arrowShortSide; + const arrowWidth = (modalPosition === 'left' || modalPosition === 'right') ? + arrowShortSide : arrowLongSide; + + const el = relativeElemRef?.current; + const modalEl = modalRef?.current; + if (!el || !modalEl) { + return {}; + } + + const buttonRect = el.getBoundingClientRect(); + const modalRect = modalEl.getBoundingClientRect(); + const modalHeight = modalRect.height; + + let top = 0; + let left = 0; + let arrowIcon = null; + let arrowTop = 0; + let arrowLeft = 0; + + switch (modalPosition) { + case 'left': + top = buttonRect.top - (modalHeight / 2) + (buttonRect.height / 2); + left = buttonRect.left - modalWidth - spaceForArrow; + arrowIcon = arrowRightIcon; + arrowTop = buttonRect.top + (buttonRect.height / 2) - (arrowHeight / 2); + arrowLeft = left + modalWidth; + break; + case 'right': + top = buttonRect.top - (modalHeight / 2) + (buttonRect.height / 2); + left = buttonRect.right + spaceForArrow; + arrowIcon = arrowLeftIcon; + arrowTop = buttonRect.top + (buttonRect.height / 2) - (arrowHeight / 2); + arrowLeft = left - arrowWidth; + break; + case 'up': + top = buttonRect.top - modalHeight - spaceForArrow; + left = buttonRect.left - ((modalWidth - buttonRect.width) / 2); + arrowIcon = arrowDownIcon; + arrowTop = top + modalHeight; + arrowLeft = buttonRect.left + (buttonRect.width / 2) - (arrowWidth / 2); + break; + case 'down': + top = buttonRect.bottom + spaceForArrow; + left = buttonRect.left - ((modalWidth - buttonRect.width) / 2); + arrowIcon = arrowUpIcon; + arrowTop = top - arrowHeight; + arrowLeft = buttonRect.left + (buttonRect.width / 2) - (arrowWidth / 2); + break; + case 'down left': + top = buttonRect.bottom + spaceForArrow; + left = buttonRect.left - modalWidth + buttonRect.width + arrowOffsetFromEnd; + arrowIcon = arrowUpIcon; + arrowTop = top - arrowHeight; + arrowLeft = buttonRect.left + (buttonRect.width / 2) - (arrowWidth / 2); + break; + case 'down right': + top = buttonRect.bottom + spaceForArrow; + left = buttonRect.left - arrowOffsetFromEnd; + arrowIcon = arrowUpIcon; + arrowTop = top - arrowHeight; + arrowLeft = buttonRect.left + (buttonRect.width / 2) - (arrowWidth / 2); + break; + case 'up left': + top = buttonRect.top - modalHeight - spaceForArrow; + left = buttonRect.left - modalWidth + buttonRect.width + arrowOffsetFromEnd; + arrowIcon = arrowDownIcon; + arrowTop = top + modalHeight; + arrowLeft = buttonRect.left + (buttonRect.width / 2) - (arrowWidth / 2); + break; + case 'up right': + top = buttonRect.top - modalHeight - spaceForArrow; + left = buttonRect.left - arrowOffsetFromEnd; + arrowIcon = arrowDownIcon; + arrowTop = top + modalHeight; + arrowLeft = buttonRect.left + (buttonRect.width / 2) - (arrowWidth / 2); + break; + } + + return {top, left, arrowIcon, arrowTop, arrowLeft}; +}; + +const ConfirmationPrompt = ({ + title, + message, + confirmLabel, + cancelLabel, + onConfirm, + onCancel, + isOpen, + relativeElemRef, + modalPosition +}) => { + const intl = useIntl(); + + const modalRef = useRef(null); + const [modalPositionValues, setModalPositionValues] = React.useState({}); + + const onModalMount = useCallback(el => { + if (!el) return; + modalRef.current = el; + + if (isOpen && relativeElemRef.current) { + const pos = calculateModalPosition(relativeElemRef, modalRef, modalPosition); + setModalPositionValues(pos); + } + }, [isOpen, relativeElemRef, modalPosition]); + + return ( + isOpen && ( + + {modalPositionValues.arrowIcon && ( + + )} + + + + + + + + + + + + + ) + ); +}; + +ConfirmationPrompt.propTypes = { + isOpen: PropTypes.bool.isRequired, + title: PropTypes.string.isRequired, + message: PropTypes.string.isRequired, + confirmLabel: PropTypes.string, + cancelLabel: PropTypes.string, + onConfirm: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, + relativeElemRef: PropTypes.object.isRequired, + modalPosition: PropTypes.oneOf([ + 'left', + 'right', + 'up', + 'down', + 'down left', + 'down right', + 'up left', + 'up right' + ]).isRequired +}; + +export default ConfirmationPrompt; diff --git a/packages/scratch-gui/src/components/confirmation-prompt/icon--arrow-down.svg b/packages/scratch-gui/src/components/confirmation-prompt/icon--arrow-down.svg new file mode 100644 index 00000000000..5c932999e6e --- /dev/null +++ b/packages/scratch-gui/src/components/confirmation-prompt/icon--arrow-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/scratch-gui/src/components/confirmation-prompt/icon--arrow-left.svg b/packages/scratch-gui/src/components/confirmation-prompt/icon--arrow-left.svg new file mode 100644 index 00000000000..0712a6f88b8 --- /dev/null +++ b/packages/scratch-gui/src/components/confirmation-prompt/icon--arrow-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/scratch-gui/src/components/confirmation-prompt/icon--arrow-right.svg b/packages/scratch-gui/src/components/confirmation-prompt/icon--arrow-right.svg new file mode 100644 index 00000000000..cc59340dc3f --- /dev/null +++ b/packages/scratch-gui/src/components/confirmation-prompt/icon--arrow-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/scratch-gui/src/components/confirmation-prompt/icon--arrow-up.svg b/packages/scratch-gui/src/components/confirmation-prompt/icon--arrow-up.svg new file mode 100644 index 00000000000..110e25bc789 --- /dev/null +++ b/packages/scratch-gui/src/components/confirmation-prompt/icon--arrow-up.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/packages/scratch-gui/src/components/gui/gui.jsx b/packages/scratch-gui/src/components/gui/gui.jsx index 15ef41cb9f6..21209037045 100644 --- a/packages/scratch-gui/src/components/gui/gui.jsx +++ b/packages/scratch-gui/src/components/gui/gui.jsx @@ -259,10 +259,6 @@ const GUIComponent = props => { isRendererSupported={isRendererSupported} isRtl={isRtl} loading={loading} - manuallySaveThumbnails={ - manuallySaveThumbnails && - userOwnsProject - } onUpdateProjectThumbnail={onUpdateProjectThumbnail} stageSize={STAGE_SIZE_MODES.large} vm={vm} @@ -349,7 +345,6 @@ const GUIComponent = props => { canManageFiles={canManageFiles} canRemix={canRemix} canSave={canSave} - canUpdateThumbnail={userOwnsProject && manuallySaveThumbnails && !isPlayerOnly} canShare={canShare} className={styles.menuBarPosition} enableCommunity={enableCommunity} @@ -371,7 +366,6 @@ const GUIComponent = props => { onShare={onShare} onStartSelectingFileUpload={onStartSelectingFileUpload} onToggleLoginOpen={onToggleLoginOpen} - onUpdateProjectThumbnail={onUpdateProjectThumbnail} userOwnsProject={userOwnsProject} username={username} accountMenuOptions={accountMenuOptions} @@ -547,6 +541,8 @@ const GUIComponent = props => { vm={vm} ariaRole="region" ariaLabel={intl.formatMessage(ariaMessages.stage)} + manuallySaveThumbnails={manuallySaveThumbnails} + isInEditor /> { - console.log('Updating thumbnail...'); - this.props.onUpdateProjectThumbnail( - projectId, - dataURItoBlob(dataURI) - ); - console.log('After dispatch'); - }); - } handleClickSeeCommunity (waitForUpdate) { if (this.props.shouldSaveBeforeTransition()) { this.props.autoUpdateProject(); // save before transitioning to project page @@ -410,13 +385,6 @@ class MenuBar extends React.Component { }; } render () { - const updateThumbnailMessage = ( - - ); const saveNowMessage = ( - {(this.props.canSave || this.props.canCreateCopy || - this.props.canRemix || this.props.canUpdateThumbnail) && ( + {(this.props.canSave || this.props.canCreateCopy || this.props.canRemix) && ( {this.props.canSave && ( @@ -546,11 +513,6 @@ class MenuBar extends React.Component { {remixMessage} )} - {this.props.canUpdateThumbnail && ( - - {updateThumbnailMessage} - - )} )} @@ -958,7 +920,6 @@ MenuBar.propTypes = { canManageFiles: PropTypes.bool, canRemix: PropTypes.bool, canSave: PropTypes.bool, - canUpdateThumbnail: PropTypes.bool, canShare: PropTypes.bool, className: PropTypes.string, confirmReadyToReplaceProject: PropTypes.func, @@ -982,7 +943,6 @@ MenuBar.propTypes = { mode220022BC: PropTypes.bool, modeMenuOpen: PropTypes.bool, modeNow: PropTypes.bool, - projectId: PropTypes.number.isRequired, onClickAbout: PropTypes.oneOfType([ PropTypes.func, // button mode: call this callback when the About button is clicked PropTypes.arrayOf( // menu mode: list of items in the About menu @@ -1021,7 +981,6 @@ MenuBar.propTypes = { onShare: PropTypes.func, onStartSelectingFileUpload: PropTypes.func, onToggleLoginOpen: PropTypes.func, - onUpdateProjectThumbnail: PropTypes.func, platform: PropTypes.oneOf(Object.keys(PLATFORM)), projectTitle: PropTypes.string, renderLogin: PropTypes.func, @@ -1034,10 +993,7 @@ MenuBar.propTypes = { accountMenuOptions: AccountMenuOptionsPropTypes, - vm: PropTypes.instanceOf(VM).isRequired, - - - storage: GUIStoragePropType, + vm: PropTypes.instanceOf(VM).isRequired }; MenuBar.defaultProps = { @@ -1050,7 +1006,6 @@ const mapStateToProps = (state, ownProps) => { const user = state.session && state.session.session && state.session.session.user; const permissions = state.session && state.session.permissions; const sessionExists = state.session && typeof state.session.session !== 'undefined'; - const storage = state.scratchGui.config.storage; return { aboutMenuOpen: aboutMenuOpen(state), @@ -1075,10 +1030,6 @@ const mapStateToProps = (state, ownProps) => { mode1990: isTimeTravel1990(state), mode2020: isTimeTravel2020(state), modeNow: isTimeTravelNow(state), - projectId: state.scratchGui.projectState.projectId, - onUpdateProjectThumbnail: - ownProps.onUpdateProjectThumbnail ?? - storage.saveProjectThumbnail?.bind(storage), platform: state.scratchGui.platform.platform, @@ -1099,9 +1050,7 @@ const mapStateToProps = (state, ownProps) => { myClassesUrl: permissions?.educator ? '/educators/classes/' : null, myClassUrl: user && permissions?.student ? `/classes/${user.classroomId}/` : null, accountSettingsUrl: '/accounts/settings/' - }, - - storage + } }; }; diff --git a/packages/scratch-gui/src/components/stage-header/icon--thumbnail.svg b/packages/scratch-gui/src/components/stage-header/icon--thumbnail.svg new file mode 100644 index 00000000000..b2986770bf9 --- /dev/null +++ b/packages/scratch-gui/src/components/stage-header/icon--thumbnail.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/scratch-gui/src/components/stage-header/stage-header.css b/packages/scratch-gui/src/components/stage-header/stage-header.css index 3b82aae0d7c..9ef095c2682 100644 --- a/packages/scratch-gui/src/components/stage-header/stage-header.css +++ b/packages/scratch-gui/src/components/stage-header/stage-header.css @@ -34,6 +34,7 @@ .stage-size-row { display: flex; + gap: 0.25rem; } .stage-size-toggle-group { @@ -46,14 +47,6 @@ border-radius: $form-radius; } -[dir="ltr"] .stage-size-toggle-group { - margin-right: .2rem; -} - -[dir="rtl"] .stage-size-toggle-group { - margin-left: .2rem; -} - .stage-button { display: block; border: 1px solid $ui-black-transparent; @@ -85,7 +78,7 @@ align-items: center; gap: 0.5rem; background-color: transparent; - + .setThumbnailButton { padding: 0.625rem 0.75rem; font-size: 0.75rem; diff --git a/packages/scratch-gui/src/components/stage-header/stage-header.jsx b/packages/scratch-gui/src/components/stage-header/stage-header.jsx index 5c69607fd97..264d7fa35ba 100644 --- a/packages/scratch-gui/src/components/stage-header/stage-header.jsx +++ b/packages/scratch-gui/src/components/stage-header/stage-header.jsx @@ -1,6 +1,6 @@ -import {FormattedMessage, defineMessages, useIntl} from 'react-intl'; +import {defineMessages, useIntl} from 'react-intl'; import PropTypes from 'prop-types'; -import React, {useCallback} from 'react'; +import React, {useCallback, useRef} from 'react'; import {connect} from 'react-redux'; import VM from '@scratch/scratch-vm'; @@ -21,6 +21,12 @@ import styles from './stage-header.css'; import {storeProjectThumbnail} from '../../lib/store-project-thumbnail.js'; import dataURItoBlob from '../../lib/data-uri-to-blob.js'; import throttle from 'lodash.throttle'; +import thumbnailIcon from './icon--thumbnail.svg'; +import { + getIsShowingWithId, + getIsUpdating +} from '../../reducers/project-state.js'; +import ConfirmationPrompt from '../confirmation-prompt/confirmation-prompt.jsx'; const messages = defineMessages({ largeStageSizeMessage: { @@ -48,6 +54,11 @@ const messages = defineMessages({ description: 'Manually save project thumbnail', id: 'gui.stageHeader.saveThumbnail' }, + setThumbnailMessage: { + defaultMessage: 'Are you sure you want to set your thumbnail?', + description: 'Confirmation message for manually saving project thumbnail', + id: 'gui.stageHeader.saveThumbnailMessage' + }, fullscreenControl: { defaultMessage: 'Full Screen Control', description: 'Button to enter/exit full screen mode', @@ -69,12 +80,16 @@ const StageHeaderComponent = function (props) { projectId, showBranding, stageSizeMode, - vm + vm, + isInEditor, + isProjectLoaded } = props; const intl = useIntl(); let header = null; + const [isThumbnailPromptOpen, setIsThumbnailPromptOpen] = React.useState(false); + const onUpdateThumbnail = useCallback( throttle( () => { @@ -91,6 +106,21 @@ const StageHeaderComponent = function (props) { [projectId, onUpdateProjectThumbnail] ); + const onThumbnailPromptOpen = useCallback(() => { + setIsThumbnailPromptOpen(true); + }, []); + + const onThumbnailPromptClose = useCallback(() => { + setIsThumbnailPromptOpen(false); + }, []); + + const onUpdateThumbnailAndClose = useCallback(() => { + onUpdateThumbnail(); + onThumbnailPromptClose(); + }, [onUpdateThumbnail]); + + const thumbnailButtonRef = useRef(null); + if (isFullScreen) { const stageDimensions = getStageDimensions(null, true); const stageButton = showBranding ? ( @@ -165,17 +195,32 @@ const StageHeaderComponent = function (props) {
+ {manuallySaveThumbnails && isInEditor && isProjectLoaded && ( + + )} + {stageControls}
- {manuallySaveThumbnails && ( - - )}
)); SaveStatus.propTypes = { alertsList: PropTypes.arrayOf(PropTypes.object), onClickSave: PropTypes.func, - projectChanged: PropTypes.bool, - formattedMessage: PropTypes.string + projectChanged: PropTypes.bool }; const mapStateToProps = state => ({ diff --git a/packages/scratch-gui/src/components/stage-header/stage-header.css b/packages/scratch-gui/src/components/stage-header/stage-header.css index 9ef095c2682..04c556e6e9c 100644 --- a/packages/scratch-gui/src/components/stage-header/stage-header.css +++ b/packages/scratch-gui/src/components/stage-header/stage-header.css @@ -78,7 +78,7 @@ align-items: center; gap: 0.5rem; background-color: transparent; - + .setThumbnailButton { padding: 0.625rem 0.75rem; font-size: 0.75rem; diff --git a/packages/scratch-gui/src/components/stage-header/stage-header.jsx b/packages/scratch-gui/src/components/stage-header/stage-header.jsx index 5e6cf399c4d..a634999aaf8 100644 --- a/packages/scratch-gui/src/components/stage-header/stage-header.jsx +++ b/packages/scratch-gui/src/components/stage-header/stage-header.jsx @@ -88,7 +88,7 @@ const StageHeaderComponent = function (props) { vm, isInEditor, isProjectLoaded, - isOwner, + userOwnsProject, showThumbnailSetting, showThumbnailSuccess, showThumbnailError @@ -209,7 +209,7 @@ const StageHeaderComponent = function (props) {
- {manuallySaveThumbnails && isInEditor && isProjectLoaded && isOwner && ( + {manuallySaveThumbnails && isInEditor && isProjectLoaded && userOwnsProject && ( @@ -235,22 +167,24 @@ const ConfirmationPrompt = ({ ConfirmationPrompt.propTypes = { isOpen: PropTypes.bool.isRequired, title: PropTypes.string.isRequired, - message: PropTypes.string.isRequired, - confirmLabel: PropTypes.string, - cancelLabel: PropTypes.string, + message: PropTypes.node.isRequired, + confirmLabel: PropTypes.node, + cancelLabel: PropTypes.node, onConfirm: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired, - relativeElemRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), - modalPosition: PropTypes.oneOf([ + relativeElementRef: PropTypes.shape({current: PropTypes.instanceOf(Element)}), + primaryPosition: PropTypes.oneOf([ + 'left', + 'right', + 'up', + 'down' + ]).isRequired, + secondaryPosition: PropTypes.oneOf([ 'left', 'right', 'up', - 'down', - 'down left', - 'down right', - 'up left', - 'up right' - ]).isRequired + 'down' + ]) }; export default ConfirmationPrompt; diff --git a/packages/scratch-gui/src/components/spinner/spinner.css b/packages/scratch-gui/src/components/spinner/spinner.css index 48601b034c6..37250b99a6f 100644 --- a/packages/scratch-gui/src/components/spinner/spinner.css +++ b/packages/scratch-gui/src/components/spinner/spinner.css @@ -12,6 +12,10 @@ box-sizing: content-box; } +.spinner.blue { + border-color: hsla(215, 100%, 65%, 0.4); +} + .spinner::before, .spinner::after { width: 1.25rem; height: 1.25rem; @@ -29,6 +33,10 @@ animation: spin 1.5s cubic-bezier(0.4, 0.1, 0.4, 1) infinite; } +.spinner.blue::after { + border-top-color: #4C97FF; +} + .small { width: .5rem; height: .5rem; diff --git a/packages/scratch-gui/src/components/spinner/spinner.jsx b/packages/scratch-gui/src/components/spinner/spinner.jsx index 6fc23ed80a8..0c3a70ad996 100644 --- a/packages/scratch-gui/src/components/spinner/spinner.jsx +++ b/packages/scratch-gui/src/components/spinner/spinner.jsx @@ -35,6 +35,7 @@ SpinnerComponent.defaultProps = { className: '', large: false, level: 'info', - small: false + small: false, + color: 'white' }; export default SpinnerComponent; diff --git a/packages/scratch-gui/src/components/stage-header/stage-header.css b/packages/scratch-gui/src/components/stage-header/stage-header.css index 04c556e6e9c..65a03140a0d 100644 --- a/packages/scratch-gui/src/components/stage-header/stage-header.css +++ b/packages/scratch-gui/src/components/stage-header/stage-header.css @@ -68,26 +68,19 @@ height: 100%; } +.stage-button-highlighted { + border-radius: 4px; + box-shadow: 0 0 0 2px #4C97FF; +} + [dir="rtl"] .stage-button-icon { transform: scaleX(-1); } -.rightSection { +.right-section { display: flex; flex-direction: row; align-items: center; gap: 0.5rem; background-color: transparent; - - .setThumbnailButton { - padding: 0.625rem 0.75rem; - font-size: 0.75rem; - line-height: 0.875rem; - color: $ui-white; - background-color: $motion-primary; - } - - .setThumbnailButton:active { - filter: brightness(90%); - } } diff --git a/packages/scratch-gui/src/components/stage-header/stage-header.jsx b/packages/scratch-gui/src/components/stage-header/stage-header.jsx index a634999aaf8..a8b18cf2236 100644 --- a/packages/scratch-gui/src/components/stage-header/stage-header.jsx +++ b/packages/scratch-gui/src/components/stage-header/stage-header.jsx @@ -1,13 +1,10 @@ -import {defineMessages, useIntl} from 'react-intl'; +import {defineMessages, FormattedMessage, useIntl} from 'react-intl'; import PropTypes from 'prop-types'; -import React, {useCallback, useRef} from 'react'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; import {connect} from 'react-redux'; import VM from '@scratch/scratch-vm'; -import { - showAlertWithTimeout, - showStandardAlert -} from '../../reducers/alerts'; +import {showAlertWithTimeout, showStandardAlert} from '../../reducers/alerts'; import Box from '../box/box.jsx'; import Button from '../button/button.jsx'; @@ -32,6 +29,8 @@ import { getIsUpdating } from '../../reducers/project-state.js'; import ConfirmationPrompt from '../confirmation-prompt/confirmation-prompt.jsx'; +import Tooltip from '../tooltip/tooltip.jsx'; +import classNames from 'classnames'; const messages = defineMessages({ largeStageSizeMessage: { @@ -64,6 +63,17 @@ const messages = defineMessages({ description: 'Confirmation message for manually saving project thumbnail', id: 'gui.stageHeader.saveThumbnailMessage' }, + thumbnailTooltipTitle: { + defaultMessage: 'Hey there! 👋', + description: 'Title for the thumbnail tooltip', + id: 'gui.stageHeader.thumbnailTooltipTitle' + }, + thumbnailTooltipBody: { + defaultMessage: 'The “{boldText}” has a new spot. The way it works is by ' + + 'taking a snapshot of your canvas and setting it as your project thumbnail.', + description: 'Body text for the thumbnail tooltip', + id: 'gui.stageHeader.thumbnailTooltipBody' + }, fullscreenControl: { defaultMessage: 'Full Screen Control', description: 'Button to enter/exit full screen mode', @@ -97,13 +107,27 @@ const StageHeaderComponent = function (props) { let header = null; - const [isThumbnailPromptOpen, setIsThumbnailPromptOpen] = React.useState(false); + const thumbnailTooltipId = 'thumbnail-tooltip'; + const thumbnailButtonRef = useRef(null); + + const [isThumbnailPromptOpen, setIsThumbnailPromptOpen] = useState(false); + const [isThumbnailTooltipOpen, setIsThumbnailTooltipOpen] = useState(false); + const [isUpdatingThumbnail, setIsUpdatingThumbnail] = useState(false); + + // To remove - new feature awareness tooltip + useEffect(() => { + if (manuallySaveThumbnails && isInEditor && isProjectLoaded && + userOwnsProject && thumbnailButtonRef.current) { + setIsThumbnailTooltipOpen(true); + } + }, [manuallySaveThumbnails, isInEditor, isProjectLoaded, userOwnsProject]); const onUpdateThumbnail = useCallback( throttle( () => { if (!onUpdateProjectThumbnail) return; + setIsUpdatingThumbnail(true); showThumbnailSetting(); storeProjectThumbnail(vm, dataURI => { @@ -112,6 +136,8 @@ const StageHeaderComponent = function (props) { showThumbnailSuccess(); } catch (e) { showThumbnailError(); + } finally { + setIsUpdatingThumbnail(false); } }); }, @@ -133,7 +159,13 @@ const StageHeaderComponent = function (props) { onThumbnailPromptClose(); }, [onUpdateThumbnail]); - const thumbnailButtonRef = useRef(null); + const onOpenTooltip = useCallback(() => { + setIsThumbnailTooltipOpen(true); + }, []); + + const onCloseTooltip = useCallback(() => { + setIsThumbnailTooltipOpen(false); + }, []); if (isFullScreen) { const stageDimensions = getStageDimensions(null, true); @@ -209,14 +241,38 @@ const StageHeaderComponent = function (props) {
+ {/* To remove - new feature awareness tooltip */} + {intl.formatMessage(messages.setThumbnail)} + }} + /> + } + /> {manuallySaveThumbnails && isInEditor && isProjectLoaded && userOwnsProject && ( diff --git a/packages/scratch-gui/src/components/stage-header/stage-header.jsx b/packages/scratch-gui/src/components/stage-header/stage-header.jsx index a8b18cf2236..9746b5232c0 100644 --- a/packages/scratch-gui/src/components/stage-header/stage-header.jsx +++ b/packages/scratch-gui/src/components/stage-header/stage-header.jsx @@ -284,7 +284,7 @@ const StageHeaderComponent = function (props) { } + message={intl.formatMessage(messages.setThumbnailMessage)} onConfirm={onUpdateThumbnailAndClose} onCancel={onThumbnailPromptClose} relativeElementRef={thumbnailButtonRef} diff --git a/packages/scratch-gui/src/components/tooltip/tooltip.css b/packages/scratch-gui/src/components/tooltip/tooltip.css index 91a8f1022f0..ddabf31150f 100644 --- a/packages/scratch-gui/src/components/tooltip/tooltip.css +++ b/packages/scratch-gui/src/components/tooltip/tooltip.css @@ -11,8 +11,6 @@ background-color: #4C97FF; opacity: 1 !important; - --tooltip-arrow-color: #4C97FF; - /* Basic Shadows/2dp */ box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.06), 0 4px 16px 0 rgba(0, 0, 0, 0.12); } @@ -36,4 +34,8 @@ font-style: normal; font-weight: 400; line-height: 20px; -} \ No newline at end of file +} + +.tooltip-arrow { + background-color: transparent; +} \ No newline at end of file diff --git a/packages/scratch-gui/src/components/tooltip/tooltip.jsx b/packages/scratch-gui/src/components/tooltip/tooltip.jsx index 48c5e793f1d..058df477908 100644 --- a/packages/scratch-gui/src/components/tooltip/tooltip.jsx +++ b/packages/scratch-gui/src/components/tooltip/tooltip.jsx @@ -77,9 +77,11 @@ const Tooltip = ({ } }; - document.addEventListener('mousedown', handleClickOutside); + // The Blockly workspace suppresses compat events like `mouseup`. + // Listen for `pointerup` instead. + document.addEventListener('pointerup', handleClickOutside); return () => { - document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener('pointerup', handleClickOutside); }; }, [isOpen, onRequestClose, targetRef]); From f4698f8a1c1435a3604c07bd3e26427ffe2fa14d Mon Sep 17 00:00:00 2001 From: Krum Angelov Date: Thu, 19 Mar 2026 08:55:19 +0200 Subject: [PATCH 09/13] chore: addressed some comments --- .../src/components/alerts/alert.css | 10 ++-- .../src/components/button/button.jsx | 5 +- .../confirmation-prompt.css | 38 ++++++------- .../confirmation-prompt.jsx | 39 +++++++++---- .../scratch-gui/src/components/gui/gui.jsx | 1 - .../components/stage-header/stage-header.jsx | 57 +++++++------------ .../stage-wrapper/stage-wrapper.jsx | 3 - .../src/components/tooltip/tooltip.css | 29 +++++----- .../src/components/tooltip/tooltip.jsx | 28 ++++++--- .../src/containers/stage-header.jsx | 33 ++++++++--- .../src/hooks/calculatePopupPosition.js | 10 ++-- packages/scratch-gui/src/lib/alerts/index.jsx | 8 +-- .../scratch-gui/src/reducers/project-state.js | 5 ++ 13 files changed, 150 insertions(+), 116 deletions(-) diff --git a/packages/scratch-gui/src/components/alerts/alert.css b/packages/scratch-gui/src/components/alerts/alert.css index 0391c2a6918..70cd10fb959 100644 --- a/packages/scratch-gui/src/components/alerts/alert.css +++ b/packages/scratch-gui/src/components/alerts/alert.css @@ -20,8 +20,8 @@ body .alert { } .alert.warn { - background: #FFF0DF; - border: 1px solid #FF8C1A; + background: $ui-white; + border: 1px solid $data-primary; box-shadow: 0px 0px 0px 2px rgba(255, 140, 26, 0.25); } @@ -31,8 +31,8 @@ body .alert { box-shadow: 0px 0px 0px 2px $extensions-light; } -.alert.blue { - border: 1px solid #4C97FF; +.alert.info-blue { + border: 1px solid $motion-primary; background: #E4F0FF; } @@ -82,7 +82,7 @@ body .alert { width: 6.5rem; padding: 0.55rem 0.9rem; border-radius: 0.35rem; - background: #FF8C1A; + background: $data-primary; color: white; font-weight: 700; font-size: 0.77rem; diff --git a/packages/scratch-gui/src/components/button/button.jsx b/packages/scratch-gui/src/components/button/button.jsx index 447f2ff8c0f..e101bddb304 100644 --- a/packages/scratch-gui/src/components/button/button.jsx +++ b/packages/scratch-gui/src/components/button/button.jsx @@ -50,7 +50,10 @@ ButtonComponent.propTypes = { iconClassName: PropTypes.string, iconSrc: PropTypes.string, onClick: PropTypes.func, - componentRef: PropTypes.func + componentRef: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.shape({current: PropTypes.instanceOf(Element)}) + ]) }; export default ButtonComponent; diff --git a/packages/scratch-gui/src/components/confirmation-prompt/confirmation-prompt.css b/packages/scratch-gui/src/components/confirmation-prompt/confirmation-prompt.css index ea5724ec0b5..8efd6d15a63 100644 --- a/packages/scratch-gui/src/components/confirmation-prompt/confirmation-prompt.css +++ b/packages/scratch-gui/src/components/confirmation-prompt/confirmation-prompt.css @@ -3,25 +3,25 @@ .modal-container { display: flex; - width: 200px; - padding: 12px; + width: 12.5rem; + padding: 0.75rem; flex-direction: column; align-items: flex-start; - border-radius: 8px; - background: #855CD6; - gap: 15px; + border-radius: 0.5rem; + background: $looks-secondary; + gap: 0.9375rem; box-sizing: border-box; } .label { align-self: stretch; - color: var(--White-White, #FFF); + color: $ui-white; text-align: center; font-family: "Helvetica Neue"; - font-size: 16px; + font-size: 1rem; font-style: normal; font-weight: 700; - line-height: 24px; + line-height: 1.5rem; } .button-row { @@ -34,12 +34,12 @@ .button-row button { all: unset; display: flex; - padding: 8px 16px; + padding: 0.5rem 1rem; justify-content: center; align-items: center; - gap: 8px; - flex: 1 0 0; - border-radius: 40px; + gap: 0.5rem; + flex: 1; + border-radius: 2.5rem; background: inherit; cursor: pointer; } @@ -49,22 +49,22 @@ } .button-row button span { - color: var(--white-white-100, #FFF); + color: $ui-white; font-family: "Helvetica Neue"; - font-size: 12px; + font-size: 0.75rem; font-style: normal; font-weight: 700; - line-height: 16px; + line-height: 1rem; } .button-row button.confirm-button { - background: var(--White-White, #FFF); + background: $ui-white; } .button-row button.confirm-button span { - color: var(--Looks-Purple-2, #855CD6); + color: $looks-secondary; } .button-row button.cancel-button { - border: 1px solid var(--white-white-100, #FFF); -} + border: 0.0625rem solid $ui-white; +} \ No newline at end of file diff --git a/packages/scratch-gui/src/components/confirmation-prompt/confirmation-prompt.jsx b/packages/scratch-gui/src/components/confirmation-prompt/confirmation-prompt.jsx index b01329213af..227f9a2f755 100644 --- a/packages/scratch-gui/src/components/confirmation-prompt/confirmation-prompt.jsx +++ b/packages/scratch-gui/src/components/confirmation-prompt/confirmation-prompt.jsx @@ -27,11 +27,11 @@ const messages = defineMessages({ } }); -const modalWidth = 200; -const spaceForArrow = 16; -const arrowOffsetFromEnd = 7; -const arrowLongSide = 29; -const arrowShortSide = 13; +const defaultModalWidth = 200; +const defaultSpaceForArrow = 16; +const defaultCounterOffset = 7; +const defaultArrowLongSide = 29; +const defaultArrowShortSide = 13; const ConfirmationPrompt = ({ title, @@ -43,7 +43,12 @@ const ConfirmationPrompt = ({ isOpen, relativeElementRef, primaryPosition, - secondaryPosition + secondaryPosition, + modalWidth = defaultModalWidth, + spaceForArrow = defaultSpaceForArrow, + counterOffset = defaultCounterOffset, + arrowLongSide = defaultArrowLongSide, + arrowShortSide = defaultArrowShortSide }) => { const intl = useIntl(); @@ -63,7 +68,7 @@ const ConfirmationPrompt = ({ arrowUpIcon, arrowDownIcon, spaceForArrow, - arrowOffsetFromEnd, + counterOffset, arrowShortSide, arrowLongSide }); @@ -79,15 +84,18 @@ const ConfirmationPrompt = ({ debouncedUpdate(); window.addEventListener('resize', debouncedUpdate); - return () => window.removeEventListener('resize', debouncedUpdate); - }, [isOpen, relativeElementRef, primaryPosition, secondaryPosition]); + return () => { + window.removeEventListener('resize', debouncedUpdate); + debouncedUpdate.cancel(); + }; + }, [isOpen, updatePosition]); const onModalMount = useCallback(el => { if (!el || !isOpen) return; modalRef.current = el; updatePosition(); - }, [isOpen, relativeElementRef, primaryPosition, secondaryPosition]); + }, [isOpen, updatePosition]); return ( isOpen && ( @@ -105,7 +113,7 @@ const ConfirmationPrompt = ({ backgroundColor: 'transparent', padding: 0, margin: 0, - position: 'absolute', + position: 'fixed', overflowX: 'hidden', zIndex: 1000 }, @@ -123,6 +131,8 @@ const ConfirmationPrompt = ({ {modalPositionValues.arrowIcon && ( { manuallySaveThumbnails={manuallySaveThumbnails} userOwnsProject={userOwnsProject} onUpdateProjectThumbnail={onUpdateProjectThumbnail} - isInEditor /> { - if (manuallySaveThumbnails && isInEditor && isProjectLoaded && + if (manuallySaveThumbnails && isProjectLoaded && userOwnsProject && thumbnailButtonRef.current) { setIsThumbnailTooltipOpen(true); } - }, [manuallySaveThumbnails, isInEditor, isProjectLoaded, userOwnsProject]); + }, [manuallySaveThumbnails, isProjectLoaded, userOwnsProject]); const onUpdateThumbnail = useCallback( throttle( @@ -128,14 +127,14 @@ const StageHeaderComponent = function (props) { if (!onUpdateProjectThumbnail) return; setIsUpdatingThumbnail(true); - showThumbnailSetting(); + onShowSettingThumbnail(); - storeProjectThumbnail(vm, dataURI => { + storeProjectThumbnail(vm, async dataURI => { try { - onUpdateProjectThumbnail(projectId, dataURItoBlob(dataURI)).then(); - showThumbnailSuccess(); + await onUpdateProjectThumbnail(projectId, dataURItoBlob(dataURI)); + onShowThumbnailSuccess(); } catch (e) { - showThumbnailError(); + onShowThumbnailError(); } finally { setIsUpdatingThumbnail(false); } @@ -143,7 +142,13 @@ const StageHeaderComponent = function (props) { }, 3000 ), - [onUpdateProjectThumbnail, projectId, showThumbnailSetting, showThumbnailSuccess, showThumbnailError, vm] + [ + onUpdateProjectThumbnail, + projectId, + onShowSettingThumbnail, + onShowThumbnailSuccess, + onShowThumbnailError + ] ); const onThumbnailPromptOpen = useCallback(() => { @@ -260,9 +265,8 @@ const StageHeaderComponent = function (props) { /> } /> - {manuallySaveThumbnails && isInEditor && isProjectLoaded && userOwnsProject && ( + {manuallySaveThumbnails && isProjectLoaded && userOwnsProject && (