(
DeleteButton.propTypes = {
className: PropTypes.string,
onClick: PropTypes.func.isRequired,
+ isConfirmationModalOpened: PropTypes.bool,
tabIndex: PropTypes.number
};
diff --git a/src/components/delete-confirmation-prompt/delete-confirmation-prompt.css b/src/components/delete-confirmation-prompt/delete-confirmation-prompt.css
new file mode 100644
index 00000000000..295bb6b5c34
--- /dev/null
+++ b/src/components/delete-confirmation-prompt/delete-confirmation-prompt.css
@@ -0,0 +1,74 @@
+@import "../../css/colors.css";
+@import "../../css/units.css";
+
+.modal-container {
+ display: flex;
+ flex-direction: row;
+ border: none;
+}
+
+.arrow-container {
+ display: flex;
+ align-items: center;
+ margin-right: -7px;
+}
+
+.arrow-container-left {
+ margin-right: -7px;
+}
+
+.arrow-container-right {
+ margin-left: -7px;
+}
+
+.body {
+ padding: 1rem 1.5rem;
+ border-radius: 0.5rem;
+ background: $looks-secondary;
+}
+
+.label {
+ color: $ui-white;
+ font-size: 1.25rem;
+ font-weight: 700;
+ margin: 1rem 0 1.5rem;
+}
+
+.button-row {
+ font-weight: bolder;
+ display: flex;
+}
+
+.button-row button {
+ display: flex;
+ gap: 0.5rem;
+ justify-content: center;
+ width: 47%;
+ padding: 0.75rem 1rem;
+ border-radius: 2rem;
+ border: 1px solid $ui-black-transparent;
+ color: $looks-secondary;
+ background: $ui-white;
+ font-weight: 600;
+ font-size: 0.85rem;
+ cursor: pointer;
+ margin: auto;
+}
+
+.button-row button.ok-button {
+ margin-left: 0;
+}
+
+.button-row button.cancel-button {
+ margin-right: 0;
+}
+
+.message {
+ margin-top: 0.25rem;
+}
+
+.delete-icon {
+ height: 1.5rem;
+ width: 1.5rem;
+}
+
diff --git a/src/components/delete-confirmation-prompt/delete-confirmation-prompt.jsx b/src/components/delete-confirmation-prompt/delete-confirmation-prompt.jsx
new file mode 100644
index 00000000000..2a6eea9525f
--- /dev/null
+++ b/src/components/delete-confirmation-prompt/delete-confirmation-prompt.jsx
@@ -0,0 +1,185 @@
+import {defineMessages, FormattedMessage, injectIntl, intlShape} from 'react-intl';
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+
+import Box from '../box/box.jsx';
+import ReactModal from 'react-modal';
+import deleteIcon from './icon--delete.svg';
+import undoIcon from './icon--undo.svg';
+import arrowLeftIcon from './icon--arrow-left.svg';
+import arrowRightIcon from './icon--arrow-right.svg';
+
+import styles from './delete-confirmation-prompt.css';
+
+// TODO: Parametrize from outside if we want more custom messaging
+const messages = defineMessages({
+ shouldDeleteSpriteMessage: {
+ defaultMessage: 'Are you sure you want to delete this sprite?',
+ description: 'Message to indicate whether selected sprite should be deleted.',
+ id: 'gui.gui.shouldDeleteSprite'
+ },
+ shouldDeleteCostumeMessage: {
+ defaultMessage: 'Are you sure you want to delete this costume?',
+ description: 'Message to indicate whether selected costume should be deleted.',
+ id: 'gui.gui.shouldDeleteCostume'
+ },
+ shouldDeleteSoundMessage: {
+ defaultMessage: 'Are you sure you want to delete this sound?',
+ description: 'Message to indicate whether selected sound should be deleted.',
+ id: 'gui.gui.shouldDeleteSound'
+ },
+ confirmOption: {
+ defaultMessage: 'yes',
+ description: 'Yes - should delete the sprite',
+ id: 'gui.gui.confirm'
+ },
+ cancelOption: {
+ defaultMessage: 'no',
+ description: 'No - cancel deletion',
+ id: 'gui.gui.cancel'
+ },
+ confirmDeletionHeading: {
+ defaultMessage: 'Confirm Asset Deletion',
+ description: 'Heading of confirmation prompt to delete asset',
+ id: 'gui.gui.deleteAssetHeading'
+ }
+});
+
+const modalWidth = 300;
+const calculateModalPosition = (relativeElemRef, modalPosition) => {
+ const refPosition = relativeElemRef.getBoundingClientRect();
+
+ if (modalPosition === 'left') {
+ return {
+ top: refPosition.top - refPosition.height,
+ left: refPosition.left - modalWidth - 25
+ };
+ }
+
+ if (modalPosition === 'right') {
+ return {
+ top: refPosition.top - refPosition.height,
+ left: refPosition.right + 25
+ };
+ }
+
+ return {};
+};
+
+const getMessage = entityType => {
+ if (entityType === 'COSTUME') {
+ return messages.shouldDeleteCostumeMessage;
+ }
+
+ if (entityType === 'SOUND') {
+ return messages.shouldDeleteSoundMessage;
+ }
+
+ return messages.shouldDeleteSpriteMessage;
+};
+
+const DeleteConfirmationPrompt = ({
+ intl,
+ onCancel,
+ onOk,
+ modalPosition,
+ entityType,
+ relativeElemRef
+}) => {
+ const modalPositionValues = calculateModalPosition(relativeElemRef, modalPosition);
+
+ return (
+
+ { modalPosition === 'right' ?
+
+
+ : null }
+
+
+
+
+
+
+
+
+
+ {modalPosition === 'left' ?
+
+
+ : null }
+
+ );
+};
+
+DeleteConfirmationPrompt.propTypes = {
+ onOk: PropTypes.func.isRequired,
+ onCancel: PropTypes.func.isRequired,
+ relativeElemRef: PropTypes.object,
+ entityType: PropTypes.string,
+ modalPosition: PropTypes.string,
+ intl: intlShape.isRequired
+};
+
+const DeleteConfirmationPromptIntl = injectIntl(DeleteConfirmationPrompt);
+
+export default DeleteConfirmationPromptIntl;
diff --git a/src/components/delete-confirmation-prompt/icon--arrow-left.svg b/src/components/delete-confirmation-prompt/icon--arrow-left.svg
new file mode 100644
index 00000000000..0712a6f88b8
--- /dev/null
+++ b/src/components/delete-confirmation-prompt/icon--arrow-left.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/components/delete-confirmation-prompt/icon--arrow-right.svg b/src/components/delete-confirmation-prompt/icon--arrow-right.svg
new file mode 100644
index 00000000000..cc59340dc3f
--- /dev/null
+++ b/src/components/delete-confirmation-prompt/icon--arrow-right.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/components/delete-confirmation-prompt/icon--delete.svg b/src/components/delete-confirmation-prompt/icon--delete.svg
new file mode 100644
index 00000000000..4be0a2a3caf
--- /dev/null
+++ b/src/components/delete-confirmation-prompt/icon--delete.svg
@@ -0,0 +1,18 @@
+
+
\ No newline at end of file
diff --git a/src/components/delete-confirmation-prompt/icon--undo.svg b/src/components/delete-confirmation-prompt/icon--undo.svg
new file mode 100644
index 00000000000..44c9517b707
--- /dev/null
+++ b/src/components/delete-confirmation-prompt/icon--undo.svg
@@ -0,0 +1,12 @@
+
+
diff --git a/src/components/library/library.css b/src/components/library/library.css
index df13ec5f71a..1419380c103 100644
--- a/src/components/library/library.css
+++ b/src/components/library/library.css
@@ -19,6 +19,24 @@
height: calc(100% - $library-header-height - $library-filter-bar-height - 2rem);
}
+.library-category {
+ display: flex;
+ flex-direction: column;
+}
+
+.library-category-title {
+ padding-Left: .5rem;
+ font-weight: bold;
+ font-size: 2rem;
+ color: $text-primary;
+}
+
+.library-category-items {
+ display: flex;
+ flex-wrap: wrap;
+ padding-bottom: 1rem;
+}
+
.filter-bar {
display: flex;
flex-direction: row;
diff --git a/src/components/library/library.jsx b/src/components/library/library.jsx
index 48a1de5541d..09aa6a33217 100644
--- a/src/components/library/library.jsx
+++ b/src/components/library/library.jsx
@@ -10,6 +10,7 @@ import Divider from '../divider/divider.jsx';
import Filter from '../filter/filter.jsx';
import TagButton from '../../containers/tag-button.jsx';
import Spinner from '../spinner/spinner.jsx';
+import {CATEGORIES} from '../../../src/lib/libraries/decks/index.jsx';
import styles from './library.css';
@@ -23,6 +24,28 @@ const messages = defineMessages({
id: 'gui.library.allTag',
defaultMessage: 'All',
description: 'Label for library tag to revert to all items after filtering by tag.'
+ },
+ // Strings here need to be defined statically
+ // https://formatjs.io/docs/getting-started/message-declaration/#pre-declaring-using-definemessage-for-later-consumption-less-recommended
+ [CATEGORIES.gettingStarted]: {
+ id: `gui.library.gettingStarted`,
+ defaultMessage: 'Getting Started',
+ description: 'Label for getting started category'
+ },
+ [CATEGORIES.basics]: {
+ id: `gui.library.basics`,
+ defaultMessage: 'Basics',
+ description: 'Label for basics category'
+ },
+ [CATEGORIES.intermediate]: {
+ id: `gui.library.intermediate`,
+ defaultMessage: 'Intermediate',
+ description: 'Label for intermediate category'
+ },
+ [CATEGORIES.prompts]: {
+ id: `gui.library.prompts`,
+ defaultMessage: 'Prompts',
+ description: 'Label for prompts category'
}
});
@@ -65,7 +88,8 @@ class LibraryComponent extends React.Component {
}
handleSelect (id) {
this.handleClose();
- this.props.onItemSelected(this.getFilteredData()[id]);
+ this.props.onItemSelected(this.getFilteredData()
+ .find(item => this.constructKey(item) === id));
}
handleClose () {
this.props.onRequestClose();
@@ -77,7 +101,8 @@ class LibraryComponent extends React.Component {
selectedTag: tag.toLowerCase()
});
} else {
- this.props.onItemMouseLeave(this.getFilteredData()[[this.state.playingItem]]);
+ this.props.onItemMouseLeave((this.getFilteredData()
+ .find(item => this.constructKey(item) === this.state.playingItem)));
this.setState({
filterQuery: '',
playingItem: null,
@@ -88,7 +113,8 @@ class LibraryComponent extends React.Component {
handleMouseEnter (id) {
// don't restart if mouse over already playing item
if (this.props.onItemMouseEnter && this.state.playingItem !== id) {
- this.props.onItemMouseEnter(this.getFilteredData()[id]);
+ this.props.onItemMouseEnter(this.getFilteredData()
+ .find(item => this.constructKey(item) === id));
this.setState({
playingItem: id
});
@@ -96,7 +122,8 @@ class LibraryComponent extends React.Component {
}
handleMouseLeave (id) {
if (this.props.onItemMouseLeave) {
- this.props.onItemMouseLeave(this.getFilteredData()[id]);
+ this.props.onItemMouseLeave(this.getFilteredData()
+ .find(item => this.constructKey(item) === id));
this.setState({
playingItem: null
});
@@ -116,7 +143,8 @@ class LibraryComponent extends React.Component {
selectedTag: ALL_TAG.tag
});
} else {
- this.props.onItemMouseLeave(this.getFilteredData()[[this.state.playingItem]]);
+ this.props.onItemMouseLeave(this.getFilteredData()
+ .find(item => this.constructKey(item) === this.state.playingItem));
this.setState({
filterQuery: event.target.value,
playingItem: null,
@@ -128,7 +156,7 @@ class LibraryComponent extends React.Component {
this.setState({filterQuery: ''});
}
getFilteredData () {
- if (this.state.selectedTag === 'all') {
+ if (this.state.selectedTag === ALL_TAG.tag) {
if (!this.state.filterQuery) return this.props.data;
return this.props.data.filter(dataItem => (
(dataItem.tags || [])
@@ -151,12 +179,68 @@ class LibraryComponent extends React.Component {
.indexOf(this.state.selectedTag) !== -1
));
}
+ constructKey (data) {
+ return typeof data.name === 'string' ? data.name : data.rawURL;
+ }
scrollToTop () {
this.filteredDataRef.scrollTop = 0;
}
setFilteredDataRef (ref) {
this.filteredDataRef = ref;
}
+ renderElement (data) {
+ const key = this.constructKey(data);
+ return (
);
+ }
+ renderData (data) {
+ if (this.state.selectedTag !== ALL_TAG.tag || !this.props.withCategories) {
+ return data.map(item => this.renderElement(item));
+ }
+
+ const dataByCategory = Object.groupBy(data, el => el.category);
+ const categoriesOrder = Object.values(CATEGORIES);
+
+ return Object.entries(dataByCategory)
+ .sort(([key1], [key2]) => categoriesOrder.indexOf(key1) - categoriesOrder.indexOf(key2))
+ .map(([key, values]) =>
+ (
+ {key === 'undefined' ?
+ null :
+
+ {this.props.intl.formatMessage(messages[key])}
+
+ }
+
+ {values.map(item => this.renderElement(item))}
+
+
));
+ }
render () {
return (
- {this.state.loaded ? this.getFilteredData().map((dataItem, index) => (
-
- )) : (
+ {this.state.loaded ? this.renderData(this.getFilteredData()) : (
(
{(props.selected && props.onDeleteButtonClick) ? (
) : null }
@@ -102,7 +103,8 @@ SpriteSelectorItem.propTypes = {
onMouseEnter: PropTypes.func,
onMouseLeave: PropTypes.func,
preventContextMenu: PropTypes.bool,
- selected: PropTypes.bool.isRequired
+ selected: PropTypes.bool.isRequired,
+ isDeleteConfirmationModalOpened: PropTypes.bool
};
export default SpriteSelectorItem;
diff --git a/src/components/sprite-selector/sprite-list.jsx b/src/components/sprite-selector/sprite-list.jsx
index 3d191029345..c496bd73259 100644
--- a/src/components/sprite-selector/sprite-list.jsx
+++ b/src/components/sprite-selector/sprite-list.jsx
@@ -94,6 +94,8 @@ const SpriteList = function (props) {
onDeleteButtonClick={onDeleteSprite}
onDuplicateButtonClick={onDuplicateSprite}
onExportButtonClick={onExportSprite}
+ withDeleteConfirmation
+ deleteConfirmationModalPosition={'left'}
/>
);
diff --git a/src/containers/sprite-selector-item.jsx b/src/containers/sprite-selector-item.jsx
index f946cf83f1d..1bedb4164da 100644
--- a/src/containers/sprite-selector-item.jsx
+++ b/src/containers/sprite-selector-item.jsx
@@ -10,6 +10,7 @@ import VM from 'scratch-vm';
import getCostumeUrl from '../lib/get-costume-url';
import DragRecognizer from '../lib/drag-recognizer';
import {getEventXY} from '../lib/touch-utils';
+import DeleteConfirmationPrompt from '../components/delete-confirmation-prompt/delete-confirmation-prompt.jsx';
import SpriteSelectorItemComponent from '../components/sprite-selector-item/sprite-selector-item.jsx';
@@ -19,8 +20,8 @@ class SpriteSelectorItem extends React.PureComponent {
bindAll(this, [
'getCostumeData',
'setRef',
+ 'setState',
'handleClick',
- 'handleDelete',
'handleDuplicate',
'handleExport',
'handleMouseEnter',
@@ -28,13 +29,18 @@ class SpriteSelectorItem extends React.PureComponent {
'handleMouseDown',
'handleDragEnd',
'handleDrag',
- 'handleTouchEnd'
+ 'handleTouchEnd',
+ 'handleDeleteButtonClick',
+ 'handleDeleteSpriteModalClose',
+ 'handleDeleteSpriteModalConfirm'
]);
this.dragRecognizer = new DragRecognizer({
onDrag: this.handleDrag,
onDragEnd: this.handleDragEnd
});
+
+ this.state = {isDeletePromptOpen: false};
}
componentDidMount () {
document.addEventListener('touchend', this.handleTouchEnd);
@@ -90,10 +96,6 @@ class SpriteSelectorItem extends React.PureComponent {
this.props.onClick(this.props.id);
}
}
- handleDelete (e) {
- e.stopPropagation(); // To prevent from bubbling back to handleClick
- this.props.onDeleteButtonClick(this.props.id);
- }
handleDuplicate (e) {
e.stopPropagation(); // To prevent from bubbling back to handleClick
this.props.onDuplicateButtonClick(this.props.id);
@@ -108,6 +110,22 @@ class SpriteSelectorItem extends React.PureComponent {
handleMouseEnter () {
this.props.dispatchSetHoveredSprite(this.props.id);
}
+ handleDeleteButtonClick (e) {
+ e.stopPropagation(); // To prevent from bubbling back to handleClick
+
+ if (this.props.withDeleteConfirmation) {
+ this.setState({isDeletePromptOpen: true});
+ } else {
+ this.props.onDeleteButtonClick(this.props.id);
+ }
+ }
+ handleDeleteSpriteModalClose () {
+ this.setState({isDeletePromptOpen: false});
+ }
+ handleDeleteSpriteModalConfirm () {
+ this.props.onDeleteButtonClick(this.props.id);
+ this.setState({isDeletePromptOpen: false});
+ }
setRef (component) {
// Access the DOM node using .elem because it is going through ContextMenuTrigger
this.ref = component && component.elem;
@@ -126,27 +144,36 @@ class SpriteSelectorItem extends React.PureComponent {
receivedBlocks,
costumeURL,
vm,
+ deleteConfirmationModalPosition,
/* eslint-enable no-unused-vars */
...props
} = this.props;
- return (
+ return (<>
+ {this.state.isDeletePromptOpen ? : null}
+ >
);
}
}
-
SpriteSelectorItem.propTypes = {
asset: PropTypes.instanceOf(storage.Asset),
costumeURL: PropTypes.string,
@@ -164,6 +191,8 @@ SpriteSelectorItem.propTypes = {
onExportButtonClick: PropTypes.func,
receivedBlocks: PropTypes.bool.isRequired,
selected: PropTypes.bool,
+ withDeleteConfirmation: PropTypes.bool,
+ deleteConfirmationModalPosition: PropTypes.string,
vm: PropTypes.instanceOf(VM).isRequired
};
diff --git a/src/containers/tips-library.jsx b/src/containers/tips-library.jsx
index 4899a7e1158..a6b2e008c59 100644
--- a/src/containers/tips-library.jsx
+++ b/src/containers/tips-library.jsx
@@ -78,6 +78,7 @@ class TipsLibrary extends React.PureComponent {
name: decksLibraryContent[id].name,
featured: true,
tags: decksLibraryContent[id].tags,
+ category: decksLibraryContent[id].category,
urlId: decksLibraryContent[id].urlId,
requiredProjectId: decksLibraryContent[id].requiredProjectId,
hidden: decksLibraryContent[id].hidden || false
@@ -94,6 +95,7 @@ class TipsLibrary extends React.PureComponent {
visible={this.props.visible}
onItemSelected={this.handleItemSelect}
onRequestClose={this.props.onRequestClose}
+ withCategories
/>
);
}
diff --git a/src/lib/libraries/decks/index.jsx b/src/lib/libraries/decks/index.jsx
index b9c48850887..b66644c100b 100644
--- a/src/lib/libraries/decks/index.jsx
+++ b/src/lib/libraries/decks/index.jsx
@@ -65,6 +65,13 @@ import addEffectsThumb from './thumbnails/add-effects.jpg';
import moveArrowKeysThumb from './thumbnails/move-arrow-keys.jpg';
import spinThumb from './thumbnails/spin.jpg';
+export const CATEGORIES = {
+ gettingStarted: 'gettingStarted',
+ basics: 'basics',
+ intermediate: 'intermediate',
+ prompts: 'prompts'
+};
+
export default {
'intro-move-sayhello': {
@@ -76,6 +83,7 @@ export default {
/>
),
tags: ['help', 'stuck', 'how', 'can', 'say'],
+ category: CATEGORIES.gettingStarted,
img: libraryIntro,
steps: [{
video: 'intro-move-sayhello'
@@ -125,6 +133,7 @@ export default {
/>
),
tags: ['help', 'stuck', 'how', 'can', 'say', 'asl', 'deaf', 'accessibile', 'hear'],
+ category: CATEGORIES.gettingStarted,
img: libraryGettingStartedASL,
steps: [{
video: 'intro-getting-started-ASL'
@@ -148,6 +157,7 @@ export default {
),
img: libraryAnimate,
tags: ['animation', 'art', 'spin', 'grow'],
+ category: CATEGORIES.prompts,
steps: [{
video: 'animate-a-name'
}, {
@@ -214,231 +224,99 @@ export default {
urlId: 'name'
},
- 'imagine': {
+ 'Animate-A-Character': {
name: (
),
- tags: ['imagine'],
- img: libraryImagine,
+ img: libraryAnimateChar,
+ category: CATEGORIES.prompts,
steps: [{
- video: 'imagine'
- }, {
- title: (
-
- ),
- image: 'imagineTypeWhatYouWant'
- }, {
- title: (
-
- ),
- image: 'imagineClickGreenFlag'
- }, {
- title: (
-
- ),
- image: 'imagineChooseBackdrop'
- }, {
- title: (
-
- ),
- image: 'imagineChooseSprite'
- }, {
- title: (
-
- ),
- image: 'imagineFlyAround'
- }, {
- title: (
-
- ),
- image: 'imagineChooseAnotherSprite'
- }, {
- title: (
-
- ),
- image: 'imagineLeftRight'
- }, {
- title: (
-
- ),
- image: 'imagineUpDown'
- }, {
- title: (
-
- ),
- image: 'imagineChangeCostumes'
- }, {
- title: (
-
- ),
- image: 'imagineGlideToPoint'
- }, {
+ video: 's228u3g5u9'
+ },
+ {
title: (
),
- image: 'imagineGrowShrink'
+ image: 'animateCharPickBackdrop'
}, {
title: (
),
- image: 'imagineChooseAnotherBackdrop'
+ image: 'animateCharPickSprite'
}, {
title: (
),
- image: 'imagineSwitchBackdrops'
+ image: 'animateCharSaySomething'
}, {
title: (
),
- image: 'imagineRecordASound'
+ image: 'animateCharAddSound'
}, {
title: (
),
- image: 'imagineChooseSound'
+ image: 'animateCharTalk'
}, {
- deckIds: [
- 'hide-and-show',
- 'Chase-Game'
- ]
- }
- ],
- urlId: 'imagine'
- },
-
- 'Make-Music': {
- name: (
-
- ),
- img: libraryMakeMusic,
- tags: ['music', 'sound', 'instrument', 'play', 'song', 'band'],
- steps: [{
- video: 'Make-Music'
- },
- {
- title: (
-
- ),
- image: 'musicPickInstrument'
- },
- {
- title: (
-
- ),
- image: 'musicPlaySound'
- },
- {
title: (
),
- image: 'musicMakeSong'
+ image: 'animateCharMove'
}, {
title: (
),
- image: 'musicMakeBeat'
+ image: 'animateCharJump'
}, {
title: (
),
- image: 'musicMakeBeatbox'
+ image: 'animateCharChangeColor'
}, {
deckIds: [
- 'add-a-backdrop',
- 'add-sprite'
+ 'code-cartoon',
+ 'Tell-A-Story'
]
}
],
- urlId: 'music'
+ urlId: 'animate-a-character'
},
'Tell-A-Story': {
@@ -450,6 +328,7 @@ export default {
/>
),
img: libraryStory,
+ category: CATEGORIES.prompts,
steps: [{
video: 'stah7jjorp'
},
@@ -554,291 +433,312 @@ export default {
urlId: 'tell-a-story'
},
- 'Chase-Game': {
+ 'say-it-out-loud': {
name: (
),
- img: libraryChaseGame,
- tags: ['games', 'arrow', 'keyboard', 'score'],
+ img: libraryTXTSpeech,
+ category: CATEGORIES.prompts,
steps: [{
- video: 'Chase-Game'
- },
- {
+ video: 'k54n8uwcty',
+ trackingPixel: (
+
+ )
+ }, {
title: (
),
- image: 'chaseGameAddBackdrop'
+ image: 'speechAddExtension'
}, {
title: (
),
- image: 'chaseGameAddSprite1'
+ image: 'speechSaySomething'
}, {
title: (
),
- image: 'chaseGameRightLeft'
+ image: 'speechSetVoice'
}, {
title: (
),
- image: 'chaseGameUpDown'
+ image: 'speechMoveAround'
}, {
title: (
),
- image: 'chaseGameAddSprite2'
+ image: 'speechAddBackdrop'
}, {
title: (
),
- image: 'chaseGameMoveRandomly'
+ image: 'speechAddSprite'
}, {
title: (
),
- image: 'chaseGamePlaySound'
+ image: 'speechSong'
}, {
title: (
),
- image: 'chaseGameAddVariable'
+ image: 'speechChangeColor'
}, {
title: (
),
- image: 'chaseGameChangeScore'
+ image: 'speechSpin'
+ }, {
+ title: (
+
+ ),
+ image: 'speechGrowShrink'
}, {
deckIds: [
- 'add-effects',
- 'move-around-with-arrow-keys'
+ 'animate-a-name',
+ 'talking'
]
}
],
- urlId: 'chase-game'
+ urlId: 'animations-that-talk'
},
- 'Animate-A-Character': {
+ 'imagine': {
name: (
),
- img: libraryAnimateChar,
+ tags: ['imagine'],
+ img: libraryImagine,
+ category: CATEGORIES.prompts,
steps: [{
- video: 's228u3g5u9'
- },
- {
+ video: 'imagine'
+ }, {
title: (
),
- image: 'animateCharPickBackdrop'
+ image: 'imagineTypeWhatYouWant'
}, {
title: (
),
- image: 'animateCharPickSprite'
+ image: 'imagineClickGreenFlag'
}, {
title: (
),
- image: 'animateCharSaySomething'
+ image: 'imagineChooseBackdrop'
}, {
title: (
),
- image: 'animateCharAddSound'
+ image: 'imagineChooseSprite'
}, {
title: (
),
- image: 'animateCharTalk'
+ image: 'imagineFlyAround'
}, {
title: (
),
- image: 'animateCharMove'
+ image: 'imagineChooseAnotherSprite'
}, {
title: (
),
- image: 'animateCharJump'
+ image: 'imagineLeftRight'
}, {
title: (
),
- image: 'animateCharChangeColor'
+ image: 'imagineUpDown'
}, {
- deckIds: [
- 'code-cartoon',
- 'Tell-A-Story'
- ]
- }
- ],
- urlId: 'animate-a-character'
- },
-
- 'Make-A-Game': {
- name: (
-
- ),
- img: libraryMakeAGame,
- tags: ['games', 'click', 'clicked', 'score'],
- steps: [{
- video: 'Make-A-Game'
- },
- {
title: (
),
- image: 'popGamePickSprite'
+ image: 'imagineChangeCostumes'
}, {
title: (
),
- image: 'popGamePlaySound'
+ image: 'imagineGlideToPoint'
}, {
title: (
),
- image: 'popGameAddScore'
+ image: 'imagineGrowShrink'
}, {
title: (
),
-
- image: 'popGameChangeScore'
+ image: 'imagineChooseAnotherBackdrop'
}, {
title: (
),
- image: 'popGameRandomPosition'
+ image: 'imagineSwitchBackdrops'
}, {
title: (
),
- image: 'popGameChangeColor'
+ image: 'imagineRecordASound'
}, {
title: (
),
- image: 'popGameResetScore'
+ image: 'imagineChooseSound'
}, {
deckIds: [
- 'add-a-backdrop',
- 'move-around-with-arrow-keys'
+ 'hide-and-show',
+ 'Chase-Game'
]
}
],
- urlId: 'clicker-game'
+ urlId: 'imagine'
},
+ 'add-effects': {
+ name: (
+
+ ),
+ tags: ['animation', 'art', 'games', 'stories', '8-bit', 'brightness', 'ghost', 'transparency', 'opacity',
+ 'fx', 'color', 'fisheye', 'whirl', 'twist', 'pixelate', 'mosaic', '8bit'],
+ category: CATEGORIES.intermediate,
+ img: addEffectsThumb,
+ steps: [{
+ video: 'add-effects'
+ }, {
+ title: (
+
+ ),
+ image: 'addEffects'
+ }, {
+ deckIds: [
+ 'add-a-backdrop',
+ 'code-cartoon'
+ ]
+ }],
+ urlId: 'add-effects'
+ },
+
+
'make-it-fly': {
name: (
),
tags: ['game', 'fly', 'how', 'can', 'animation'],
+ category: CATEGORIES.intermediate,
img: libraryMakeFly,
steps: [{
video: 'zbtdx2dem9'
@@ -968,7 +869,78 @@ export default {
],
urlId: 'make-it-fly'
},
-
+
+ 'Make-Music': {
+ name: (
+
+ ),
+ img: libraryMakeMusic,
+ tags: ['music', 'sound', 'instrument', 'play', 'song', 'band'],
+ category: CATEGORIES.intermediate,
+ steps: [{
+ video: 'Make-Music'
+ },
+ {
+ title: (
+
+ ),
+ image: 'musicPickInstrument'
+ },
+ {
+ title: (
+
+ ),
+ image: 'musicPlaySound'
+ },
+ {
+ title: (
+
+ ),
+ image: 'musicMakeSong'
+ }, {
+ title: (
+
+ ),
+ image: 'musicMakeBeat'
+ }, {
+ title: (
+
+ ),
+ image: 'musicMakeBeatbox'
+ }, {
+ deckIds: [
+ 'add-a-backdrop',
+ 'add-sprite'
+ ]
+ }
+ ],
+ urlId: 'music'
+ },
+
'pong': {
name: (
+ ),
+ image: 'pongBounceAround'
+ }, {
+ title: (
+
+ ),
+ image: 'pongAddPaddle'
+ }, {
+ title: (
+
+ ),
+ image: 'pongMoveThePaddle'
+ }, {
+ title: (
+
+ ),
+ image: 'pongSelectBallSprite'
+ }, {
+ title: (
+
+ ),
+ image: 'pongAddMoreCodeToBall'
+ }, {
+ title: (
+
+ ),
+ image: 'pongAddAScore'
+ }, {
+ title: (
+
+ ),
+ image: 'pongChooseScoreFromMenu'
+ }, {
+ title: (
+
+ ),
+ image: 'pongInsertChangeScoreBlock'
+ }, {
+ title: (
+
+ ),
+ image: 'pongResetScore'
+ }, {
+ title: (
+
+ ),
+ image: 'pongAddLineSprite'
+ }, {
+ title: (
+
+ ),
+ image: 'pongGameOver'
+ }, {
+ deckIds: [
+ 'add-effects',
+ 'Video-Sensing'
+ ]
+ }
+ ],
+ urlId: 'pong'
+ },
+
+ 'Make-A-Game': {
+ name: (
+
+ ),
+ img: libraryMakeAGame,
+ tags: ['games', 'click', 'clicked', 'score'],
+ category: CATEGORIES.prompts,
+ steps: [{
+ video: 'Make-A-Game'
+ },
+ {
+ title: (
+
+ ),
+ image: 'popGamePickSprite'
+ }, {
+ title: (
+
+ ),
+ image: 'popGamePlaySound'
+ }, {
+ title: (
+
+ ),
+ image: 'popGameAddScore'
+ }, {
+ title: (
+
+ ),
+
+ image: 'popGameChangeScore'
+ }, {
+ title: (
+
+ ),
+ image: 'popGameRandomPosition'
+ }, {
+ title: (
+
),
- image: 'pongBounceAround'
+ image: 'popGameChangeColor'
}, {
title: (
),
- image: 'pongAddPaddle'
+ image: 'popGameResetScore'
}, {
+ deckIds: [
+ 'add-a-backdrop',
+ 'move-around-with-arrow-keys'
+ ]
+ }
+ ],
+ urlId: 'clicker-game'
+ },
+
+ 'Chase-Game': {
+ name: (
+
+ ),
+ img: libraryChaseGame,
+ tags: ['games', 'arrow', 'keyboard', 'score'],
+ category: CATEGORIES.prompts,
+ steps: [{
+ video: 'Chase-Game'
+ },
+ {
title: (
),
- image: 'pongMoveThePaddle'
+ image: 'chaseGameAddBackdrop'
}, {
title: (
),
- image: 'pongSelectBallSprite'
+ image: 'chaseGameAddSprite1'
}, {
title: (
),
- image: 'pongAddMoreCodeToBall'
+ image: 'chaseGameRightLeft'
}, {
title: (
),
- image: 'pongAddAScore'
+ image: 'chaseGameUpDown'
}, {
title: (
),
- image: 'pongChooseScoreFromMenu'
+ image: 'chaseGameAddSprite2'
}, {
title: (
),
- image: 'pongInsertChangeScoreBlock'
+ image: 'chaseGameMoveRandomly'
}, {
title: (
),
- image: 'pongResetScore'
+ image: 'chaseGamePlaySound'
}, {
title: (
),
- image: 'pongAddLineSprite'
+ image: 'chaseGameAddVariable'
}, {
title: (
),
- image: 'pongGameOver'
+ image: 'chaseGameChangeScore'
}, {
deckIds: [
'add-effects',
- 'Video-Sensing'
+ 'move-around-with-arrow-keys'
]
}
],
- urlId: 'pong'
+ urlId: 'chase-game'
},
'code-cartoon': {
@@ -1120,6 +1286,7 @@ export default {
tags: ['code-cartoon'],
requiredProjectId: '331474033',
img: libraryCodeCartoon,
+ category: CATEGORIES.prompts,
steps: [{
video: 'code-cartoon'
}, {
@@ -1241,6 +1408,7 @@ export default {
),
requiredProjectId: '249143200',
img: libraryCartoonNetwork,
+ category: CATEGORIES.prompts,
steps: [{
video: 'uz5oz5h9yg',
trackingPixel: (
@@ -1314,194 +1482,81 @@ export default {
- ),
- image: 'cnBackdrop'
- },
- {
- video: '6o76f5ivo1'
- },
- {
- deckIds: [
- 'switch-costume',
- 'add-effects'
- ]
- }
- ],
- urlId: 'animate-an-adventure-game'
- },
-
- 'Video-Sensing': {
- name: (
-
- ),
- img: libraryVideoSens,
- steps: [{
- video: '3pd1z110d6'
- },
- {
- title: (
-
- ),
- image: 'videoAddExtension'
- }, {
- title: (
-
- ),
- image: 'videoPet'
- }, {
- title: (
-
- ),
- image: 'videoAnimate'
- }, {
- title: (
-
- ),
- image: 'videoPop'
- }, {
- deckIds: [
- 'Make-Music',
- 'add-effects'
- ]
- }
- ],
- urlId: 'video-sensing'
- },
-
- 'say-it-out-loud': {
- name: (
-
- ),
- img: libraryTXTSpeech,
- steps: [{
- video: 'k54n8uwcty',
- trackingPixel: (
-
- )
- }, {
- title: (
-
- ),
- image: 'speechAddExtension'
- }, {
- title: (
-
- ),
- image: 'speechSaySomething'
- }, {
- title: (
-
- ),
- image: 'speechSetVoice'
- }, {
- title: (
-
- ),
- image: 'speechMoveAround'
- }, {
- title: (
-
- ),
- image: 'speechAddBackdrop'
- }, {
- title: (
-
),
- image: 'speechAddSprite'
- }, {
+ image: 'cnBackdrop'
+ },
+ {
+ video: '6o76f5ivo1'
+ },
+ {
+ deckIds: [
+ 'switch-costume',
+ 'add-effects'
+ ]
+ }
+ ],
+ urlId: 'animate-an-adventure-game'
+ },
+
+ 'Video-Sensing': {
+ name: (
+
+ ),
+ img: libraryVideoSens,
+ category: CATEGORIES.intermediate,
+ steps: [{
+ video: '3pd1z110d6'
+ },
+ {
title: (
),
- image: 'speechSong'
+ image: 'videoAddExtension'
}, {
title: (
),
- image: 'speechChangeColor'
+ image: 'videoPet'
}, {
title: (
),
- image: 'speechSpin'
+ image: 'videoAnimate'
}, {
title: (
),
- image: 'speechGrowShrink'
+ image: 'videoPop'
}, {
deckIds: [
- 'animate-a-name',
- 'talking'
+ 'Make-Music',
+ 'add-effects'
]
}
],
- urlId: 'animations-that-talk'
+ urlId: 'video-sensing'
},
'talking': {
@@ -1513,6 +1568,7 @@ export default {
/>
),
tags: ['talking'],
+ category: CATEGORIES.intermediate,
img: libraryTalking,
steps: [{
video: 'talking'
@@ -1653,6 +1709,7 @@ export default {
),
img: libraryAddSprite,
tags: ['art', 'games', 'stories', 'character'],
+ category: CATEGORIES.gettingStarted,
steps: [
{
title: (
@@ -1684,6 +1741,7 @@ export default {
),
img: addBackdropThumb,
tags: ['art', 'games', 'stories', 'background'],
+ category: CATEGORIES.gettingStarted,
steps: [{
video: 'add-a-backdrop'
}, {
@@ -1706,6 +1764,46 @@ export default {
urlId: 'add-a-backdrop'
},
+ 'move-around-with-arrow-keys': {
+ name: (
+
+ ),
+ img: moveArrowKeysThumb,
+ tags: ['games', 'keyboard'],
+ category: CATEGORIES.basics,
+ steps: [{
+ video: 'move-around-with-arrow-keys'
+ }, {
+ title: (
+
+ ),
+ image: 'moveArrowKeysLeftRight'
+ }, {
+ title: (
+
+ ),
+ image: 'moveArrowKeysUpDown'
+ }, {
+ deckIds: [
+ 'make-it-fly',
+ 'switch-costume'
+ ]
+ }],
+ urlId: 'arrow-keys'
+ },
+
'change-size': {
name: (
),
img: changeSizeThumb,
+ category: CATEGORIES.basics,
scale: ['art', 'animation', 'scale'],
steps: [{
video: 'change-size'
@@ -1746,6 +1845,7 @@ export default {
),
img: glideAroundThumb,
tags: ['animation', 'stories', 'music', 'instrument', 'play', 'song', 'band'],
+ category: CATEGORIES.basics,
steps: [{
video: 'glide-around'
}, {
@@ -1775,6 +1875,46 @@ export default {
urlId: 'glide-around'
},
+ 'spin-video': {
+ name: (
+
+ ),
+ img: spinThumb,
+ tags: ['animation', 'rotate', 'rotation'],
+ category: CATEGORIES.basics,
+ steps: [{
+ video: 'spin-video'
+ }, {
+ title: (
+
+ ),
+ image: 'spinTurn'
+ }, {
+ title: (
+
+ ),
+ image: 'spinPointInDirection'
+ }, {
+ deckIds: [
+ 'add-a-backdrop',
+ 'switch-costume'
+ ]
+ }],
+ urlId: 'make-it-spin'
+ },
+
'record-a-sound': {
name: (
),
tags: ['music', 'games', 'stories'],
+ category: CATEGORIES.basics,
img: recordASound,
steps: [{
video: 'record-a-sound'
@@ -1841,45 +1982,6 @@ export default {
urlId: 'record-a-sound'
},
- 'spin-video': {
- name: (
-
- ),
- img: spinThumb,
- tags: ['animation', 'rotate', 'rotation'],
- steps: [{
- video: 'spin-video'
- }, {
- title: (
-
- ),
- image: 'spinTurn'
- }, {
- title: (
-
- ),
- image: 'spinPointInDirection'
- }, {
- deckIds: [
- 'add-a-backdrop',
- 'switch-costume'
- ]
- }],
- urlId: 'make-it-spin'
- },
-
'hide-and-show': {
name: (
- ),
- img: moveArrowKeysThumb,
- tags: ['games', 'keyboard'],
- steps: [{
- video: 'move-around-with-arrow-keys'
- }, {
- title: (
-
- ),
- image: 'moveArrowKeysLeftRight'
- }, {
- title: (
-
- ),
- image: 'moveArrowKeysUpDown'
- }, {
- deckIds: [
- 'make-it-fly',
- 'switch-costume'
- ]
- }],
- urlId: 'arrow-keys'
- },
-
- 'add-effects': {
- name: (
-
- ),
- tags: ['animation', 'art', 'games', 'stories', '8-bit', 'brightness', 'ghost', 'transparency', 'opacity',
- 'fx', 'color', 'fisheye', 'whirl', 'twist', 'pixelate', 'mosaic', '8bit'],
- img: addEffectsThumb,
- steps: [{
- video: 'add-effects'
- }, {
- title: (
-
- ),
- image: 'addEffects'
- }, {
- deckIds: [
- 'add-a-backdrop',
- 'code-cartoon'
- ]
- }],
- urlId: 'add-effects'
- },
-
'wedo2-getting-started': {
steps: [{
video: '4im7iizv47'
diff --git a/test/integration/how-tos.test.js b/test/integration/how-tos.test.js
index 577031f37d6..494d7ce4ef5 100644
--- a/test/integration/how-tos.test.js
+++ b/test/integration/how-tos.test.js
@@ -27,8 +27,8 @@ describe('Working with the how-to library', () => {
await loadUri(uri);
await clickText('Costumes');
await clickXpath('//*[@aria-label="Tutorials"]');
- await clickText('Getting Started'); // Modal should close
- // Make sure YouTube video on first card appears
+ await clickText('Add a Backdrop'); // Modal should close
+ // Make sure YouTube video on card appears
await findByXpath('//div[contains(@class, "step-video")]');
const logs = await getLogs();
await expect(logs).toEqual([]);
diff --git a/test/integration/menu-bar.test.js b/test/integration/menu-bar.test.js
index 45d6ec4e7c9..33a6353e61c 100644
--- a/test/integration/menu-bar.test.js
+++ b/test/integration/menu-bar.test.js
@@ -84,6 +84,7 @@ describe('Menu bar settings', () => {
// Change the project by deleting a sprite
await rightClickText('Sprite1', scope.spriteTile);
await clickText('delete', scope.spriteTile);
+ await clickText('yes', scope.modal);
await clickXpath(FILE_MENU_XPATH);
await clickText('Load from your computer');
diff --git a/test/integration/sprites.test.js b/test/integration/sprites.test.js
index 40677246cda..c798e0a1792 100644
--- a/test/integration/sprites.test.js
+++ b/test/integration/sprites.test.js
@@ -68,6 +68,7 @@ describe('Working with sprites', () => {
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
await rightClickText('Sprite1', scope.spriteTile);
await clickText('delete', scope.spriteTile);
+ await clickText('yes', scope.modal);
// Confirm that the stage has been switched to
await findByText('Stage selected: no motion blocks');
const logs = await getLogs();
@@ -78,6 +79,7 @@ describe('Working with sprites', () => {
await loadUri(uri);
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
await clickXpath('//*[@aria-label="Delete"]'); // Only visible close button is on the sprite
+ await clickText('yes', scope.modal);
// Confirm that the stage has been switched to
await findByText('Stage selected: no motion blocks');
const logs = await getLogs();
diff --git a/test/integration/stage-size.test.js b/test/integration/stage-size.test.js
index ca235905079..0c3510140ff 100644
--- a/test/integration/stage-size.test.js
+++ b/test/integration/stage-size.test.js
@@ -27,12 +27,14 @@ describe('Loading scratch gui', () => {
test('Switching small/large stage after highlighting and deleting sprite', async () => {
await loadUri(uri);
- // Highlight the sprite
- await clickText('Sprite1', scope.spriteTile);
+ await new Promise((r) => setTimeout(r, 500)); // wait for animation
+
// Delete it
await rightClickText('Sprite1', scope.spriteTile);
+
await clickText('delete', scope.spriteTile);
+ await clickText('yes', scope.modal);
// Go to small stage mode
await clickXpath('//button[@title="Switch to small stage"]');
diff --git a/test/unit/components/__snapshots__/sprite-selector-item.test.jsx.snap b/test/unit/components/__snapshots__/sprite-selector-item.test.jsx.snap
index f7f19434a61..14936897676 100644
--- a/test/unit/components/__snapshots__/sprite-selector-item.test.jsx.snap
+++ b/test/unit/components/__snapshots__/sprite-selector-item.test.jsx.snap
@@ -53,7 +53,7 @@ exports[`SpriteSelectorItemComponent matches snapshot when given a number and de
tabIndex={0}
>
jest.fn(() => null));
describe('SpriteSelectorItem Container', () => {
const mockStore = configureStore();
let className;
@@ -17,11 +20,12 @@ describe('SpriteSelectorItem Container', () => {
let selected;
let id;
let store;
+ let vm;
// Wrap this in a function so it gets test specific states and can be reused.
- const getContainer = function () {
+ const getContainer = function (withDeleteConfirmation) {
return (
- {
selected={selected}
onClick={onClick}
onDeleteButtonClick={onDeleteButtonClick}
+ vm={vm}
+ withDeleteConfirmation={withDeleteConfirmation}
/>
);
};
beforeEach(() => {
- store = mockStore({scratchGui: {
- hoveredTarget: {receivedBlocks: false, sprite: null},
- assetDrag: {dragging: false}
- }});
className = 'ponies';
costumeURL = 'https://scratch.mit.edu/foo/bar/pony';
id = 1337;
@@ -48,11 +50,29 @@ describe('SpriteSelectorItem Container', () => {
onDeleteButtonClick = jest.fn();
dispatchSetHoveredSprite = jest.fn();
selected = true;
+ vm = new VM();
+ store = mockStore({scratchGui: {
+ hoveredTarget: {receivedBlocks: false, sprite: null},
+ assetDrag: {dragging: false},
+ vm
+ }});
});
- test('should delete the sprite', () => {
+ test('should delete the sprite, when called without `withDeleteConfirmation`', () => {
const wrapper = mountWithIntl(getContainer());
+
wrapper.find(DeleteButton).simulate('click');
+ expect(DeleteConfirmationPrompt).not.toHaveBeenCalled();
expect(onDeleteButtonClick).toHaveBeenCalledWith(1337);
});
+
+ test('should initiate sprite deletion, when called `withDeleteConfirmation`', () => {
+ const wrapper = mountWithIntl(getContainer(true));
+
+ expect(DeleteConfirmationPrompt).not.toHaveBeenCalled();
+
+ wrapper.find(DeleteButton).simulate('click');
+
+ expect(DeleteConfirmationPrompt).toHaveBeenCalled();
+ });
});