Commit 4efbf000 authored by chrisgarrity's avatar chrisgarrity

Merge remote-tracking branch 'origin/develop' into release/2021-06-30

parents 859dda96 40a01524
This diff is collapsed.
...@@ -69,8 +69,10 @@ const getTopLevelComments = (id, offset, ownerUsername, isAdmin, token) => (disp ...@@ -69,8 +69,10 @@ const getTopLevelComments = (id, offset, ownerUsername, isAdmin, token) => (disp
} }
dispatch(setFetchStatus('comments', Status.FETCHED)); dispatch(setFetchStatus('comments', Status.FETCHED));
dispatch(setComments(body)); dispatch(setComments(body));
dispatch(getReplies(id, body.map(comment => comment.id), 0, ownerUsername, isAdmin, token)); const commentsWithReplies = body.filter(comment => comment.reply_count > 0);
if (commentsWithReplies.length > 0) {
dispatch(getReplies(id, commentsWithReplies.map(comment => comment.id), 0, ownerUsername, isAdmin, token));
}
// If we loaded a full page of comments, assume there are more to load. // If we loaded a full page of comments, assume there are more to load.
// This will be wrong (1 / COMMENT_LIMIT) of the time, but does not require // This will be wrong (1 / COMMENT_LIMIT) of the time, but does not require
// any more server query complexity, so seems worth it. In the case of a project with // any more server query complexity, so seems worth it. In the case of a project with
...@@ -105,7 +107,9 @@ const getCommentById = (projectId, commentId, ownerUsername, isAdmin, token) => ...@@ -105,7 +107,9 @@ const getCommentById = (projectId, commentId, ownerUsername, isAdmin, token) =>
// If the comment is not a reply, show it as top level and load replies // If the comment is not a reply, show it as top level and load replies
dispatch(setFetchStatus('comments', Status.FETCHED)); dispatch(setFetchStatus('comments', Status.FETCHED));
dispatch(setComments([body])); dispatch(setComments([body]));
if (body.reply_count > 0) {
dispatch(getReplies(projectId, [body.id], 0, ownerUsername, isAdmin, token)); dispatch(getReplies(projectId, [body.id], 0, ownerUsername, isAdmin, token));
}
}); });
}); });
......
...@@ -90,7 +90,10 @@ const getTopLevelComments = () => ((dispatch, getState) => { ...@@ -90,7 +90,10 @@ const getTopLevelComments = () => ((dispatch, getState) => {
} }
dispatch(setFetchStatus('comments', Status.FETCHED)); dispatch(setFetchStatus('comments', Status.FETCHED));
dispatch(setComments(body)); dispatch(setComments(body));
dispatch(getReplies(body.map(comment => comment.id), 0)); const commentsWithReplies = body.filter(comment => comment.reply_count > 0);
if (commentsWithReplies.length > 0) {
dispatch(getReplies(commentsWithReplies.map(comment => comment.id), 0));
}
// If we loaded a full page of comments, assume there are more to load. // If we loaded a full page of comments, assume there are more to load.
// This will be wrong (1 / COMMENT_LIMIT) of the time, but does not require // This will be wrong (1 / COMMENT_LIMIT) of the time, but does not require
...@@ -130,7 +133,9 @@ const getCommentById = commentId => ((dispatch, getState) => { ...@@ -130,7 +133,9 @@ const getCommentById = commentId => ((dispatch, getState) => {
// If the comment is not a reply, show it as top level and load replies // If the comment is not a reply, show it as top level and load replies
dispatch(setFetchStatus('comments', Status.FETCHED)); dispatch(setFetchStatus('comments', Status.FETCHED));
dispatch(setComments([body])); dispatch(setComments([body]));
if (body.reply_count > 0) {
dispatch(getReplies(body.id, 0)); dispatch(getReplies(body.id, 0));
}
}); });
}); });
......
...@@ -304,10 +304,10 @@ ...@@ -304,10 +304,10 @@
}, },
{ {
"name": "studio", "name": "studio",
"pattern": "^/studios-playground/\\d+(/projects|/curators|/activity|/comments)?/?(\\?.*)?$", "pattern": "^/studios/\\d+(/projects|/curators|/activity|/comments)?/?(\\?.*)?$",
"routeAlias": "/studios-playground/?$", "routeAlias": "/studios/?$",
"view": "studio/studio", "view": "studio/studio",
"title": "Studio Playground", "title": "Scratch Studio",
"dynamicMetaTags": true "dynamicMetaTags": true
}, },
{ {
......
...@@ -29,8 +29,7 @@ ...@@ -29,8 +29,7 @@
"studio.projectsHeader": "Projects", "studio.projectsHeader": "Projects",
"studio.addProjectsHeader": "Add Projects", "studio.addProjectsHeader": "Add Projects",
"studio.addProject": "Add", "studio.addProject": "Add by URL",
"studio.addProjectPlaceholder": "Project URL",
"studio.openToAll": "Anyone can add projects", "studio.openToAll": "Anyone can add projects",
......
@import "../../../colors"; @import "../../../colors";
.promote-modal { .promote-modal {
width: 680px;
.promote-title { .promote-title {
background: $ui-blue; background: $ui-blue;
border-top-left-radius: 12px; border-top-left-radius: 12px;
......
...@@ -3,6 +3,7 @@ import React, {useContext, useState} from 'react'; ...@@ -3,6 +3,7 @@ import React, {useContext, useState} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import AlertContext from '../../../components/alert/alert-context.js'; import AlertContext from '../../../components/alert/alert-context.js';
import {errorToMessageId} from '../studio-project-adder.jsx';
const UserProjectsTile = ({id, title, image, inStudio, onAdd, onRemove}) => { const UserProjectsTile = ({id, title, image, inStudio, onAdd, onRemove}) => {
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
...@@ -10,17 +11,19 @@ const UserProjectsTile = ({id, title, image, inStudio, onAdd, onRemove}) => { ...@@ -10,17 +11,19 @@ const UserProjectsTile = ({id, title, image, inStudio, onAdd, onRemove}) => {
const {errorAlert} = useContext(AlertContext); const {errorAlert} = useContext(AlertContext);
const toggle = () => { const toggle = () => {
setSubmitting(true); setSubmitting(true);
(added ? onRemove(id) : onAdd(id)) const adding = !added; // for clarity, the current action is opposite of previous state
(adding ? onAdd(id) : onRemove(id))
.then(() => { .then(() => {
setAdded(!added); setAdded(adding);
setSubmitting(false); setSubmitting(false);
}) })
.catch(() => { .catch(e => {
// if adding, use the same error messages as the add-by-url component
// otherwise use a single generic message for remove errors
const errorId = adding ? errorToMessageId(e) :
'studio.alertProjectRemoveError';
setSubmitting(false); setSubmitting(false);
errorAlert({ errorAlert({id: errorId}, null);
id: added ? 'studio.alertProjectRemoveError' :
'studio.alertProjectAddError'
}, null);
}); });
}; };
return ( return (
......
...@@ -3,7 +3,7 @@ import React, {useState} from 'react'; ...@@ -3,7 +3,7 @@ import React, {useState} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import classNames from 'classnames'; import classNames from 'classnames';
import {FormattedMessage, intlShape, injectIntl} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import {Errors, addProject} from './lib/studio-project-actions'; import {Errors, addProject} from './lib/studio-project-actions';
import UserProjectsModal from './modals/user-projects-modal.jsx'; import UserProjectsModal from './modals/user-projects-modal.jsx';
...@@ -23,7 +23,7 @@ const errorToMessageId = error => { ...@@ -23,7 +23,7 @@ const errorToMessageId = error => {
} }
}; };
const StudioProjectAdder = ({intl, onSubmit}) => { const StudioProjectAdder = ({onSubmit}) => {
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
...@@ -67,7 +67,7 @@ const StudioProjectAdder = ({intl, onSubmit}) => { ...@@ -67,7 +67,7 @@ const StudioProjectAdder = ({intl, onSubmit}) => {
className={classNames({'mod-form-error': error})} className={classNames({'mod-form-error': error})}
disabled={submitting} disabled={submitting}
type="text" type="text"
placeholder={intl.formatMessage({id: 'studio.addProjectPlaceholder'})} placeholder="https://scratch.mit.edu/projects/xxxx"
value={value} value={value}
onKeyDown={e => e.key === 'Enter' && submit()} onKeyDown={e => e.key === 'Enter' && submit()}
onChange={e => setValue(e.target.value)} onChange={e => setValue(e.target.value)}
...@@ -93,8 +93,7 @@ const StudioProjectAdder = ({intl, onSubmit}) => { ...@@ -93,8 +93,7 @@ const StudioProjectAdder = ({intl, onSubmit}) => {
}; };
StudioProjectAdder.propTypes = { StudioProjectAdder.propTypes = {
onSubmit: PropTypes.func, onSubmit: PropTypes.func
intl: intlShape
}; };
const mapStateToProps = () => ({}); const mapStateToProps = () => ({});
...@@ -103,4 +102,6 @@ const mapDispatchToProps = ({ ...@@ -103,4 +102,6 @@ const mapDispatchToProps = ({
onSubmit: addProject onSubmit: addProject
}); });
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(StudioProjectAdder)); export default connect(mapStateToProps, mapDispatchToProps)(StudioProjectAdder);
export {errorToMessageId};
...@@ -52,8 +52,8 @@ describe('www-integration project rows', () => { ...@@ -52,8 +52,8 @@ describe('www-integration project rows', () => {
test('Featured Studios link', async () => { test('Featured Studios link', async () => {
await clickXpath('//div[@class="box"][descendant::text()="Featured Studios"]' + await clickXpath('//div[@class="box"][descendant::text()="Featured Studios"]' +
'//div[contains(@class, "thumbnail")][1]/a[@class="thumbnail-image"]'); '//div[contains(@class, "thumbnail")][1]/a[@class="thumbnail-image"]');
let galleryInfo = await findByXpath('//div[contains(@class, "gallery-info")]'); let studioInfo = await findByXpath('//div[contains(@class, "studio-info")]');
let galleryInfoDisplayed = await galleryInfo.isDisplayed(); let studioInfoDisplayed = await studioInfo.isDisplayed();
await expect(galleryInfoDisplayed).toBe(true); await expect(studioInfoDisplayed).toBe(true);
}); });
}); });
...@@ -92,7 +92,7 @@ describe('www-integration my_stuff', () => { ...@@ -92,7 +92,7 @@ describe('www-integration my_stuff', () => {
await clickXpath('//form[@id="new_studio"]/button[@type="submit"]'); await clickXpath('//form[@id="new_studio"]/button[@type="submit"]');
await driver.sleep(500); await driver.sleep(500);
// my stuff also has an element with the id tabs // my stuff also has an element with the id tabs
let tabs = await findByXpath('//ul[@id="tabs" and @class="tabs-index box-h-tabs h-tabs"]'); let tabs = await findByXpath('//div[@class="studio-tabs"]');
let tabsVisible = await tabs.isDisplayed(); let tabsVisible = await tabs.isDisplayed();
expect(tabsVisible).toBe(true); expect(tabsVisible).toBe(true);
}); });
......
import SeleniumHelper from './selenium-helpers.js';
const {
findByXpath,
buildDriver
} = new SeleniumHelper();
let remote = process.env.SMOKE_REMOTE || false;
let rootUrl = process.env.ROOT_URL || 'https://scratch.ly';
let studioId = process.env.TEST_STUDIO_ID || 10004360;
let studioUrl = rootUrl + '/studios/' + studioId;
if (remote){
jest.setTimeout(60000);
} else {
jest.setTimeout(20000);
}
let driver;
describe('studio page while signed out', () => {
beforeAll(async () => {
// expect(projectUrl).toBe(defined);
driver = await buildDriver('www-integration studio-page signed out');
await driver.get(rootUrl);
});
beforeEach(async () => {
await driver.get(studioUrl);
let studioNav = await findByXpath('//div[@class="studio-tabs"]');
await studioNav.isDisplayed();
});
afterAll(async () => await driver.quit());
test('land on projects tab', async () => {
await driver.get(studioUrl);
let projectGrid = await findByXpath('//div[@class="studio-projects-grid"]');
let projectGridDisplayed = await projectGrid.isDisplayed();
await expect(projectGridDisplayed).toBe(true);
});
test('studio title', async () => {
let studioTitle = await findByXpath('//div[@class="studio-title"]');
let titleText = await studioTitle.getText();
await expect(titleText).toEqual('studio for automated testing');
});
test('studio description', async () => {
let xpath = '//div[contains(@class, "studio-description")]';
let studioDescription = await findByXpath(xpath);
let descriptionText = await studioDescription.getText();
await expect(descriptionText).toEqual('a description');
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment