mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-23 15:47:53 -05:00
Studio admin panel
This commit is contained in:
parent
8984f2cedc
commit
b38a97adf3
4 changed files with 199 additions and 0 deletions
96
src/views/studio/studio-admin-panel.jsx
Normal file
96
src/views/studio/studio-admin-panel.jsx
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
/* eslint-disable react/jsx-no-bind */
|
||||||
|
import React, {useState, useEffect} from 'react';
|
||||||
|
import {connect} from 'react-redux';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
|
||||||
|
import {selectIsAdmin} from '../../redux/session.js';
|
||||||
|
import {selectStudioId} from '../../redux/studio.js';
|
||||||
|
|
||||||
|
import AdminPanel from '../../components/adminpanel/adminpanel.jsx';
|
||||||
|
|
||||||
|
const adminPanelOpenKey = 'adminPanelToggled_studios';
|
||||||
|
const adminPanelOpenClass = 'mod-view-admin-panel-open';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Propagate the admin panel openness to localStorage and set a class name
|
||||||
|
* on the #view element.
|
||||||
|
* @param {boolean} value - whether the admin panel is now open.
|
||||||
|
*/
|
||||||
|
const storeAdminPanelOpen = value => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(adminPanelOpenKey, value ? 'open' : 'closed');
|
||||||
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('Could not set adminPanelToggled_studios in local storage', e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
document.querySelector('#view').classList
|
||||||
|
.toggle(adminPanelOpenClass, value);
|
||||||
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('Could not set admin-panel-open class on #view');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the previous stored value of admin panel openness from localStorage.
|
||||||
|
* @returns {boolean} - whether the admin panel should be open.
|
||||||
|
*/
|
||||||
|
const getAdminPanelOpen = () => {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(adminPanelOpenKey) === 'open';
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const StudioAdminPanel = ({studioId, showAdminPanel}) => {
|
||||||
|
const [adminPanelOpen, setAdminPanelOpen] = useState(getAdminPanelOpen());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
storeAdminPanelOpen(adminPanelOpen);
|
||||||
|
}, [adminPanelOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showAdminPanel) return;
|
||||||
|
const handleMessage = e => {
|
||||||
|
if (e.data === 'closePanel') setAdminPanelOpen(false);
|
||||||
|
};
|
||||||
|
window.addEventListener('message', handleMessage);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('message', handleMessage);
|
||||||
|
};
|
||||||
|
}, [showAdminPanel]);
|
||||||
|
|
||||||
|
return showAdminPanel && (
|
||||||
|
<AdminPanel
|
||||||
|
className={classNames('studio-admin-panel', {
|
||||||
|
'admin-panel-open': adminPanelOpen
|
||||||
|
})}
|
||||||
|
isOpen={adminPanelOpen}
|
||||||
|
onOpen={() => setAdminPanelOpen(true)}
|
||||||
|
>
|
||||||
|
<iframe
|
||||||
|
className="admin-iframe"
|
||||||
|
src={`/scratch2-studios/${studioId}/adminpanel/`}
|
||||||
|
/>
|
||||||
|
</AdminPanel>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConnnectedStudioAdminPanel = connect(
|
||||||
|
state => ({
|
||||||
|
studioId: selectStudioId(state),
|
||||||
|
showAdminPanel: selectIsAdmin(state)
|
||||||
|
})
|
||||||
|
)(StudioAdminPanel);
|
||||||
|
|
||||||
|
export {
|
||||||
|
ConnnectedStudioAdminPanel as default,
|
||||||
|
// Export the unconnected component by name for testing
|
||||||
|
StudioAdminPanel,
|
||||||
|
// Export some constants for easy testing as well
|
||||||
|
adminPanelOpenClass,
|
||||||
|
adminPanelOpenKey
|
||||||
|
};
|
|
@ -22,6 +22,7 @@ import StudioManagers from './studio-managers.jsx';
|
||||||
import StudioCurators from './studio-curators.jsx';
|
import StudioCurators from './studio-curators.jsx';
|
||||||
import StudioComments from './studio-comments.jsx';
|
import StudioComments from './studio-comments.jsx';
|
||||||
import StudioActivity from './studio-activity.jsx';
|
import StudioActivity from './studio-activity.jsx';
|
||||||
|
import StudioAdminPanel from './studio-admin-panel.jsx';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
projects,
|
projects,
|
||||||
|
@ -88,6 +89,7 @@ const ConnectedStudioShell = connect(
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<Page className="studio-page">
|
<Page className="studio-page">
|
||||||
|
<StudioAdminPanel />
|
||||||
<Router>
|
<Router>
|
||||||
<Switch>
|
<Switch>
|
||||||
{/* Use variable studioPath to support /studio-playground/ or future route */}
|
{/* Use variable studioPath to support /studio-playground/ or future route */}
|
||||||
|
|
|
@ -15,6 +15,12 @@ $radius: 8px;
|
||||||
display: block;
|
display: block;
|
||||||
padding-top: 40px;
|
padding-top: 40px;
|
||||||
|
|
||||||
|
&.mod-view-admin-panel-open {
|
||||||
|
min-width: unset;
|
||||||
|
width: calc(100% - 250px);
|
||||||
|
margin: 50px 0px 50px 250px;
|
||||||
|
}
|
||||||
|
|
||||||
/* WAT Why does everything center at smaller widths??!! */
|
/* WAT Why does everything center at smaller widths??!! */
|
||||||
@media #{$intermediate-and-smaller} {
|
@media #{$intermediate-and-smaller} {
|
||||||
& {
|
& {
|
||||||
|
@ -349,6 +355,29 @@ $radius: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.studio-admin-panel {
|
||||||
|
margin-top: 51px;
|
||||||
|
border: 0;
|
||||||
|
padding: .5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-admin-panel.admin-panel-open {
|
||||||
|
padding: 0;
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-iframe {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 100;
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
width: 250px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
/* Modification classes for different interaction states */
|
/* Modification classes for different interaction states */
|
||||||
.mod-fetching { /* When a field has no content to display yet */
|
.mod-fetching { /* When a field has no content to display yet */
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
72
test/unit/components/studio-admin-panel.test.jsx
Normal file
72
test/unit/components/studio-admin-panel.test.jsx
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {act} from 'react-dom/test-utils';
|
||||||
|
|
||||||
|
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
|
||||||
|
import AdminPanel from '../../../src/components/adminpanel/adminpanel.jsx';
|
||||||
|
import {
|
||||||
|
StudioAdminPanel, adminPanelOpenClass, adminPanelOpenKey
|
||||||
|
} from '../../../src/views/studio/studio-admin-panel.jsx';
|
||||||
|
|
||||||
|
let viewEl;
|
||||||
|
describe('Studio comments', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
viewEl = global.document.createElement('div');
|
||||||
|
viewEl.id = 'view';
|
||||||
|
global.document.body.appendChild(viewEl);
|
||||||
|
});
|
||||||
|
beforeEach(() => {
|
||||||
|
global.localStorage.clear();
|
||||||
|
viewEl.classList.remove(adminPanelOpenClass);
|
||||||
|
});
|
||||||
|
describe('gets stored state from local storage if available', () => {
|
||||||
|
test('stored as open', () => {
|
||||||
|
global.localStorage.setItem(adminPanelOpenKey, 'open');
|
||||||
|
const component = mountWithIntl(<StudioAdminPanel showAdminPanel />);
|
||||||
|
const child = component.find(AdminPanel);
|
||||||
|
expect(child.prop('isOpen')).toBe(true);
|
||||||
|
});
|
||||||
|
test('stored as closed', () => {
|
||||||
|
global.localStorage.setItem(adminPanelOpenKey, 'closed');
|
||||||
|
const component = mountWithIntl(<StudioAdminPanel showAdminPanel />);
|
||||||
|
const child = component.find(AdminPanel);
|
||||||
|
expect(child.prop('isOpen')).toBe(false);
|
||||||
|
});
|
||||||
|
test('not stored', () => {
|
||||||
|
const component = mountWithIntl(
|
||||||
|
<StudioAdminPanel showAdminPanel />
|
||||||
|
);
|
||||||
|
const child = component.find(AdminPanel);
|
||||||
|
expect(child.prop('isOpen')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('calling onOpen sets a class on the #viewEl and records in local storage', () => {
|
||||||
|
const component = mountWithIntl(<StudioAdminPanel showAdminPanel />);
|
||||||
|
let child = component.find(AdminPanel);
|
||||||
|
expect(viewEl.classList.contains(adminPanelOpenClass)).toBe(false);
|
||||||
|
// `act` is a test-util function for making react state updates sync
|
||||||
|
act(child.prop('onOpen'));
|
||||||
|
expect(viewEl.classList.contains(adminPanelOpenClass)).toBe(true);
|
||||||
|
expect(global.localStorage.getItem(adminPanelOpenKey)).toBe('open');
|
||||||
|
});
|
||||||
|
test('renders the correct iframe when open', () => {
|
||||||
|
global.localStorage.setItem(adminPanelOpenKey, 'open');
|
||||||
|
const component = mountWithIntl(
|
||||||
|
<StudioAdminPanel
|
||||||
|
studioId={123}
|
||||||
|
showAdminPanel
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
let child = component.find('iframe');
|
||||||
|
expect(child.getDOMNode().src).toMatch('/scratch2-studios/123/adminpanel');
|
||||||
|
});
|
||||||
|
test('responds to closePanel MessageEvent from the iframe', () => {
|
||||||
|
global.localStorage.setItem(adminPanelOpenKey, 'open');
|
||||||
|
mountWithIntl(<StudioAdminPanel showAdminPanel />);
|
||||||
|
expect(viewEl.classList.contains(adminPanelOpenClass)).toBe(true);
|
||||||
|
act(() => {
|
||||||
|
global.window.dispatchEvent(new global.MessageEvent('message', {data: 'closePanel'}));
|
||||||
|
});
|
||||||
|
expect(viewEl.classList.contains(adminPanelOpenClass)).toBe(false);
|
||||||
|
expect(global.localStorage.getItem(adminPanelOpenKey)).toBe('closed');
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue