Skip to content
This repository was archived by the owner on May 18, 2026. It is now read-only.

Commit 45f3ef9

Browse files
authored
Merge pull request #515 from smalruby/feat/issue-18-extension-filter
feat: add default hidden functionality to extension library
2 parents aa8fec2 + 0490ae2 commit 45f3ef9

18 files changed

Lines changed: 236 additions & 69 deletions

File tree

cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"gapi",
66
"googleusercontent",
77
"Hira",
8+
"mbit",
89
"smalrubot"
910
],
1011
"ignorePaths": [

src/components/koshien-test-modal/koshien-test-modal.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,16 @@
2121
flex: 1;
2222
min-height: 0;
2323
}
24+
25+
.reload-button {
26+
font-weight: normal;
27+
font-size: 0.75rem;
28+
}
29+
30+
.stop-icon {
31+
position: relative;
32+
margin: 0.25rem;
33+
user-select: none;
34+
transform-origin: 50%;
35+
transform: rotate(45deg);
36+
}

src/components/koshien-test-modal/koshien-test-modal.jsx

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import PropTypes from 'prop-types';
22
import React from 'react';
3-
import {defineMessages, injectIntl, intlShape} from 'react-intl';
3+
import {defineMessages, injectIntl, intlShape, FormattedMessage} from 'react-intl';
44

55
import Box from '../box/box.jsx';
6+
import Button from '../button/button.jsx';
67
import Modal from '../../containers/modal.jsx';
78

9+
import reloadIcon from '../../lib/assets/icon--reload.svg';
10+
import stopIcon from '../close-button/icon--close.svg';
11+
812
import styles from './koshien-test-modal.css';
913

1014
const messages = defineMessages({
@@ -33,16 +37,42 @@ const KoshienTestModal = props => {
3337
setLoading(false);
3438
}, []);
3539

40+
const headerActions = loading ? (
41+
<Button
42+
className={styles.reloadButton}
43+
iconClassName={styles.stopIcon}
44+
iconSrc={stopIcon}
45+
onClick={handleStop}
46+
>
47+
<FormattedMessage
48+
defaultMessage="Stop"
49+
description="Stop button in modal"
50+
id="gui.modal.stop"
51+
/>
52+
</Button>
53+
) : (
54+
<Button
55+
className={styles.reloadButton}
56+
iconSrc={reloadIcon}
57+
onClick={handleReload}
58+
>
59+
<FormattedMessage
60+
defaultMessage="Reload"
61+
description="Reload button in modal"
62+
id="gui.modal.reload"
63+
/>
64+
</Button>
65+
);
66+
3667
return (
3768
<Modal
3869
className={styles.modalContent}
3970
contentLabel={intl.formatMessage(messages.title)}
4071
fullScreen
72+
headerActions={headerActions}
4173
headerClassName={styles.header}
4274
id="koshienTestModal"
4375
loading={loading}
44-
onReload={handleReload}
45-
onStop={handleStop}
4676
onRequestClose={onRequestClose}
4777
>
4878
<Box

src/components/library/library.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ class LibraryComponent extends React.Component {
199199
<Modal
200200
fullScreen
201201
contentLabel={this.props.title}
202+
headerActions={this.props.headerActions}
202203
id={this.props.id}
203204
onRequestClose={this.handleClose}
204205
>
@@ -275,6 +276,7 @@ LibraryComponent.propTypes = {
275276
/* eslint-enable react/no-unused-prop-types, lines-around-comment */
276277
),
277278
filterable: PropTypes.bool,
279+
headerActions: PropTypes.node,
278280
id: PropTypes.string.isRequired,
279281
intl: intlShape.isRequired,
280282
onItemMouseEnter: PropTypes.func,

src/components/modal/modal.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,20 @@ $sides: 20rem;
156156
margin-right: -4.75rem;
157157
}
158158

159+
/* Header actions (generic) */
160+
.header-item-actions {
161+
padding: 0;
162+
z-index: 1;
163+
}
164+
165+
[dir="ltr"] .header-item-actions {
166+
margin-left: -4.75rem;
167+
}
168+
169+
[dir="rtl"] .header-item-actions {
170+
margin-right: -4.75rem;
171+
}
172+
159173
.reload-button {
160174
font-weight: normal;
161175
font-size: 0.75rem;

src/components/modal/modal.jsx

Lines changed: 5 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import CloseButton from '../close-button/close-button.jsx';
1010

1111
import backIcon from '../../lib/assets/icon--back.svg';
1212
import helpIcon from '../../lib/assets/icon--help.svg';
13-
import reloadIcon from '../../lib/assets/icon--reload.svg';
14-
import stopIcon from '../close-button/icon--close.svg';
1513

1614
import styles from './modal.css';
1715

@@ -66,46 +64,16 @@ const ModalComponent = props => (
6664
) : null}
6765
{props.contentLabel}
6866
</div>
69-
{props.loading && props.onStop ? (
67+
{props.headerActions ? (
7068
<div
7169
className={classNames(
7270
styles.headerItem,
73-
styles.headerItemReload
71+
styles.headerItemActions
7472
)}
7573
>
76-
<Button
77-
className={styles.reloadButton}
78-
iconClassName={styles.stopIcon}
79-
iconSrc={stopIcon}
80-
onClick={props.onStop}
81-
>
82-
<FormattedMessage
83-
defaultMessage="Stop"
84-
description="Stop button in modal"
85-
id="gui.modal.stop"
86-
/>
87-
</Button>
74+
{props.headerActions}
8875
</div>
89-
) : (props.onReload ? (
90-
<div
91-
className={classNames(
92-
styles.headerItem,
93-
styles.headerItemReload
94-
)}
95-
>
96-
<Button
97-
className={styles.reloadButton}
98-
iconSrc={reloadIcon}
99-
onClick={props.onReload}
100-
>
101-
<FormattedMessage
102-
defaultMessage="Reload"
103-
description="Reload button in modal"
104-
id="gui.modal.reload"
105-
/>
106-
</Button>
107-
</div>
108-
) : null)}
76+
) : null}
10977
<div
11078
className={classNames(
11179
styles.headerItem,
@@ -150,13 +118,12 @@ ModalComponent.propTypes = {
150118
PropTypes.object
151119
]).isRequired,
152120
fullScreen: PropTypes.bool,
121+
headerActions: PropTypes.node,
153122
headerClassName: PropTypes.string,
154123
headerImage: PropTypes.string,
155124
isRtl: PropTypes.bool,
156125
loading: PropTypes.bool,
157126
onHelp: PropTypes.func,
158-
onReload: PropTypes.func,
159-
onStop: PropTypes.func,
160127
onRequestClose: PropTypes.func
161128
};
162129

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
@import "../components/modal/modal.css";
2+
3+
.show-all-extensions-label {
4+
display: flex;
5+
align-items: center;
6+
font-size: 0.75rem;
7+
font-weight: normal;
8+
cursor: pointer;
9+
user-select: none;
10+
color: $ui-white;
11+
white-space: nowrap;
12+
padding: 1rem;
13+
}
14+
15+
.show-all-extensions-checkbox {
16+
cursor: pointer;
17+
}
18+
19+
[dir="ltr"] .show-all-extensions-checkbox {
20+
margin-right: 0.5rem;
21+
}
22+
23+
[dir="rtl"] .show-all-extensions-checkbox {
24+
margin-left: 0.5rem;
25+
}

src/containers/extension-library.jsx

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@ import bindAll from 'lodash.bindall';
22
import PropTypes from 'prop-types';
33
import React from 'react';
44
import VM from 'scratch-vm';
5-
import {defineMessages, injectIntl, intlShape} from 'react-intl';
5+
import {connect} from 'react-redux';
6+
import {defineMessages, injectIntl, intlShape, FormattedMessage} from 'react-intl';
67

78
import extensionLibraryContent from '../lib/libraries/extensions/index.jsx';
89

910
import LibraryComponent from '../components/library/library.jsx';
1011
import extensionIcon from '../components/action-menu/icon--sprite.svg';
1112

13+
import {toggleShowAllExtensions} from '../reducers/extension-filter';
14+
15+
import styles from './extension-library.css';
16+
1217
const messages = defineMessages({
1318
extensionTitle: {
1419
defaultMessage: 'Choose an Extension',
@@ -37,9 +42,13 @@ class ExtensionLibrary extends React.PureComponent {
3742
}
3843
});
3944
bindAll(this, [
40-
'handleItemSelect'
45+
'handleItemSelect',
46+
'handleToggleShowAllExtensions'
4147
]);
4248
}
49+
handleToggleShowAllExtensions (event) {
50+
this.props.onToggleShowAllExtensions(event.target.checked);
51+
}
4352
handleItemSelect (item) {
4453
const id = item.extensionId;
4554
let url = item.extensionURL ? item.extensionURL : id;
@@ -62,18 +71,54 @@ class ExtensionLibrary extends React.PureComponent {
6271
const extensionsParam = query.get('extensions') || '';
6372
const showMeshV2 = extensionsParam.split(',').includes('meshV2');
6473

74+
const showAllExtensionsParam = query.get('showAllExtensions');
75+
const showAllExtensions = showAllExtensionsParam === 'true' ? true :
76+
showAllExtensionsParam === 'false' ? false :
77+
this.props.showAllExtensions;
78+
6579
const extensionLibraryThumbnailData = extensionLibraryContent
66-
.filter(extension => (
67-
extension.extensionId !== 'meshV2' || showMeshV2
68-
))
80+
.filter(extension => {
81+
if (extension.extensionId === 'meshV2' && !showMeshV2) {
82+
return false;
83+
}
84+
if (!showAllExtensions && extension.defaultHidden) {
85+
return false;
86+
}
87+
return true;
88+
})
6989
.map(extension => ({
7090
rawURL: extension.iconURL || extensionIcon,
7191
...extension
7292
}));
93+
94+
const checkboxLabel = this.props.intl.formatMessage({
95+
defaultMessage: 'Show all extensions',
96+
description: 'Checkbox label to show all extensions including hidden ones',
97+
id: 'gui.extensionLibrary.showAllExtensions'
98+
});
99+
100+
const headerActions = (
101+
<label className={styles.showAllExtensionsLabel}>
102+
<input
103+
aria-label={checkboxLabel}
104+
checked={showAllExtensions}
105+
className={styles.showAllExtensionsCheckbox}
106+
type="checkbox"
107+
onChange={this.handleToggleShowAllExtensions}
108+
/>
109+
<FormattedMessage
110+
defaultMessage="Show all extensions"
111+
description="Checkbox label to show all extensions including hidden ones"
112+
id="gui.extensionLibrary.showAllExtensions"
113+
/>
114+
</label>
115+
);
116+
73117
return (
74118
<LibraryComponent
75119
data={extensionLibraryThumbnailData}
76120
filterable={false}
121+
headerActions={headerActions}
77122
id="extensionLibrary"
78123
title={this.props.intl.formatMessage(messages.extensionTitle)}
79124
visible={this.props.visible}
@@ -88,8 +133,21 @@ ExtensionLibrary.propTypes = {
88133
intl: intlShape.isRequired,
89134
onCategorySelected: PropTypes.func,
90135
onRequestClose: PropTypes.func,
136+
onToggleShowAllExtensions: PropTypes.func,
137+
showAllExtensions: PropTypes.bool,
91138
visible: PropTypes.bool,
92139
vm: PropTypes.instanceOf(VM).isRequired // eslint-disable-line react/no-unused-prop-types
93140
};
94141

95-
export default injectIntl(ExtensionLibrary);
142+
const mapStateToProps = state => ({
143+
showAllExtensions: state.scratchGui.extensionFilter.showAllExtensions
144+
});
145+
146+
const mapDispatchToProps = dispatch => ({
147+
onToggleShowAllExtensions: showAll => dispatch(toggleShowAllExtensions(showAll))
148+
});
149+
150+
export default connect(
151+
mapStateToProps,
152+
mapDispatchToProps
153+
)(injectIntl(ExtensionLibrary));

src/containers/library-item.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ LibraryItem.propTypes = {
157157
md5ext: PropTypes.string // 3.0 library format
158158
})
159159
),
160-
id: PropTypes.number.isRequired,
160+
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
161161
insetIconURL: PropTypes.string,
162162
internetConnectionRequired: PropTypes.bool,
163163
isPlaying: PropTypes.bool,

0 commit comments

Comments
 (0)