Studio admin panel

This commit is contained in:
Paul Kaplan 2021-05-17 09:05:02 -04:00
parent 8984f2cedc
commit b38a97adf3
4 changed files with 199 additions and 0 deletions

View 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
};

View file

@ -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 */}

View file

@ -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;

View 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');
});
});