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
}
dispatch(setFetchStatus('comments', Status.FETCHED));
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.
// 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
......@@ -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
dispatch(setFetchStatus('comments', Status.FETCHED));
dispatch(setComments([body]));
dispatch(getReplies(projectId, [body.id], 0, ownerUsername, isAdmin, token));
if (body.reply_count > 0) {
dispatch(getReplies(projectId, [body.id], 0, ownerUsername, isAdmin, token));
}
});
});
......
......@@ -90,7 +90,10 @@ const getTopLevelComments = () => ((dispatch, getState) => {
}
dispatch(setFetchStatus('comments', Status.FETCHED));
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.
// This will be wrong (1 / COMMENT_LIMIT) of the time, but does not require
......@@ -130,7 +133,9 @@ const getCommentById = commentId => ((dispatch, getState) => {
// If the comment is not a reply, show it as top level and load replies
dispatch(setFetchStatus('comments', Status.FETCHED));
dispatch(setComments([body]));
dispatch(getReplies(body.id, 0));
if (body.reply_count > 0) {
dispatch(getReplies(body.id, 0));
}
});
});
......
......@@ -304,10 +304,10 @@
},
{
"name": "studio",
"pattern": "^/studios-playground/\\d+(/projects|/curators|/activity|/comments)?/?(\\?.*)?$",
"routeAlias": "/studios-playground/?$",
"pattern": "^/studios/\\d+(/projects|/curators|/activity|/comments)?/?(\\?.*)?$",
"routeAlias": "/studios/?$",
"view": "studio/studio",
"title": "Studio Playground",
"title": "Scratch Studio",
"dynamicMetaTags": true
},
{
......
......@@ -29,8 +29,7 @@
"studio.projectsHeader": "Projects",
"studio.addProjectsHeader": "Add Projects",
"studio.addProject": "Add",
"studio.addProjectPlaceholder": "Project URL",
"studio.addProject": "Add by URL",
"studio.openToAll": "Anyone can add projects",
......
@import "../../../colors";
.promote-modal {
width: 680px;
.promote-title {
background: $ui-blue;
border-top-left-radius: 12px;
......
......@@ -3,6 +3,7 @@ import React, {useContext, useState} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import AlertContext from '../../../components/alert/alert-context.js';
import {errorToMessageId} from '../studio-project-adder.jsx';
const UserProjectsTile = ({id, title, image, inStudio, onAdd, onRemove}) => {
const [submitting, setSubmitting] = useState(false);
......@@ -10,17 +11,19 @@ const UserProjectsTile = ({id, title, image, inStudio, onAdd, onRemove}) => {
const {errorAlert} = useContext(AlertContext);
const toggle = () => {
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(() => {
setAdded(!added);
setAdded(adding);
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);
errorAlert({
id: added ? 'studio.alertProjectRemoveError' :
'studio.alertProjectAddError'
}, null);
errorAlert({id: errorId}, null);
});
};
return (
......
......@@ -3,7 +3,7 @@ import React, {useState} from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
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 UserProjectsModal from './modals/user-projects-modal.jsx';
......@@ -23,7 +23,7 @@ const errorToMessageId = error => {
}
};
const StudioProjectAdder = ({intl, onSubmit}) => {
const StudioProjectAdder = ({onSubmit}) => {
const [value, setValue] = useState('');
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState(null);
......@@ -67,7 +67,7 @@ const StudioProjectAdder = ({intl, onSubmit}) => {
className={classNames({'mod-form-error': error})}
disabled={submitting}
type="text"
placeholder={intl.formatMessage({id: 'studio.addProjectPlaceholder'})}
placeholder="https://scratch.mit.edu/projects/xxxx"
value={value}
onKeyDown={e => e.key === 'Enter' && submit()}
onChange={e => setValue(e.target.value)}
......@@ -93,8 +93,7 @@ const StudioProjectAdder = ({intl, onSubmit}) => {
};
StudioProjectAdder.propTypes = {
onSubmit: PropTypes.func,
intl: intlShape
onSubmit: PropTypes.func
};
const mapStateToProps = () => ({});
......@@ -103,4 +102,6 @@ const mapDispatchToProps = ({
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', () => {
test('Featured Studios link', async () => {
await clickXpath('//div[@class="box"][descendant::text()="Featured Studios"]' +
'//div[contains(@class, "thumbnail")][1]/a[@class="thumbnail-image"]');
let galleryInfo = await findByXpath('//div[contains(@class, "gallery-info")]');
let galleryInfoDisplayed = await galleryInfo.isDisplayed();
await expect(galleryInfoDisplayed).toBe(true);
let studioInfo = await findByXpath('//div[contains(@class, "studio-info")]');
let studioInfoDisplayed = await studioInfo.isDisplayed();
await expect(studioInfoDisplayed).toBe(true);
});
});
......@@ -92,7 +92,7 @@ describe('www-integration my_stuff', () => {
await clickXpath('//form[@id="new_studio"]/button[@type="submit"]');
await driver.sleep(500);
// 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();
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