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 StudioComments from './studio-comments.jsx';
|
||||
import StudioActivity from './studio-activity.jsx';
|
||||
import StudioAdminPanel from './studio-admin-panel.jsx';
|
||||
|
||||
import {
|
||||
projects,
|
||||
|
@ -88,6 +89,7 @@ const ConnectedStudioShell = connect(
|
|||
|
||||
render(
|
||||
<Page className="studio-page">
|
||||
<StudioAdminPanel />
|
||||
<Router>
|
||||
<Switch>
|
||||
{/* Use variable studioPath to support /studio-playground/ or future route */}
|
||||
|
|
|
@ -15,6 +15,12 @@ $radius: 8px;
|
|||
display: block;
|
||||
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??!! */
|
||||
@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 */
|
||||
.mod-fetching { /* When a field has no content to display yet */
|
||||
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