Skip to content
Snippets Groups Projects
Unverified Commit 1fdb2838 authored by Jack Reed's avatar Jack Reed Committed by GitHub
Browse files

Merge pull request #1922 from ProjectMirador/1899-normalize-companion-window-state

Normalize companion window state
parents 3ae0038e a34930de
Branches
Tags
No related merge requests found
Showing
with 349 additions and 126 deletions
import * as actions from '../../../src/state/actions';
import ActionTypes from '../../../src/state/actions/action-types';
describe('companionWindow actions', () => {
describe('addCompanionWindow', () => {
it('should return correct action object', () => {
const payload = {
content: 'info',
position: 'right',
foo: 'bar',
};
const action = actions.addCompanionWindow(payload);
expect(action.type).toBe(ActionTypes.ADD_COMPANION_WINDOW);
expect(action.payload).toEqual(payload);
});
it('should set the correct default values', () => {
const payload = {};
const defaults = { foo: 'bar' };
const action = actions.addCompanionWindow(payload, defaults);
expect(action.payload.foo).toBe('bar');
});
it('should generate a new companionWindow ID', () => {
const payload = {};
expect(actions.addCompanionWindow(payload).id).toEqual(
expect.stringMatching(/^cw-\w+-\w+/),
);
});
});
describe('updateCompanionWindow', () => {
it('should return correct action object', () => {
const payload = {
content: 'info',
position: 'right',
};
const action = actions.updateCompanionWindow('cw-123', payload);
expect(action.type).toBe(ActionTypes.UPDATE_COMPANION_WINDOW);
expect(action.id).toBe('cw-123');
expect(action.payload).toEqual(payload);
});
});
describe('removeCompanionWindow', () => {
it('should return correct action object', () => {
const action = actions.removeCompanionWindow('cw-123');
expect(action.type).toBe(ActionTypes.REMOVE_COMPANION_WINDOW);
expect(action.id).toBe('cw-123');
});
});
});
......@@ -15,6 +15,7 @@ describe('window actions', () => {
id: 'helloworld',
canvasIndex: 1,
collectionIndex: 0,
companionWindowIds: [],
manifestId: null,
rangeId: null,
thumbnailNavigationPosition: 'bottom',
......@@ -26,6 +27,20 @@ describe('window actions', () => {
expect(actions.addWindow(options)).toEqual(expectedAction);
});
});
describe('updateWindow', () => {
it('should return correct action object', () => {
const payload = {
foo: 1,
bar: 2,
};
const action = actions.updateWindow('window-123', payload);
expect(action.type).toBe(ActionTypes.UPDATE_WINDOW);
expect(action.id).toBe('window-123');
expect(action.payload).toEqual(payload);
});
});
describe('removeWindow', () => {
it('removes the window and returns windowId', () => {
const id = 'abc123';
......@@ -85,36 +100,43 @@ describe('window actions', () => {
});
});
describe('setWindowCompanionWindow', () => {
it('returns the appropriate action type', () => {
const windowId = 'abc123';
const panelType = 'info';
const position = 'right';
const expectedAction = {
type: ActionTypes.SET_WINDOW_COMPANION_WINDOW,
windowId,
panelType,
position,
};
expect(actions.setWindowCompanionWindow(windowId, 'info', 'right')).toEqual(expectedAction);
});
});
describe('popOutCompanionWindow', () => {
it('returns a thunk which dispatches the appropriate actions', () => {
const mockDispatch = jest.fn();
const mockState = {
windows: {
abc123: {
companionWindowIds: ['cw-1'],
},
},
};
const mockDispatch = jest.fn(() => ({ id: 'cw-1' }));
const mockGetState = jest.fn(() => mockState);
const windowId = 'abc123';
const panelType = 'info';
const position = 'right';
const thunk = actions.popOutCompanionWindow(windowId, panelType, position);
expect(typeof thunk).toEqual('function');
thunk(mockDispatch);
expect(mockDispatch).toHaveBeenCalledTimes(2);
expect(mockDispatch).toHaveBeenCalledWith({
type: ActionTypes.SET_WINDOW_COMPANION_WINDOW, windowId, panelType, position,
thunk(mockDispatch, mockGetState);
expect(mockDispatch).toHaveBeenCalledTimes(4);
expect(mockDispatch).toHaveBeenNthCalledWith(1, {
type: ActionTypes.REMOVE_COMPANION_WINDOW,
id: 'cw-1',
});
const addCompanionWindowAction = mockDispatch.mock.calls[1][0];
expect(addCompanionWindowAction.type).toBe(ActionTypes.ADD_COMPANION_WINDOW);
expect(addCompanionWindowAction.payload).toEqual({ content: 'info', position: 'right' });
expect(addCompanionWindowAction.id.startsWith('cw-')).toBe(true);
expect(mockDispatch).toHaveBeenNthCalledWith(3, {
type: ActionTypes.UPDATE_WINDOW,
id: 'abc123',
payload: { companionWindowIds: ['cw-1'] },
});
expect(mockDispatch).toHaveBeenCalledWith({
expect(mockDispatch).toHaveBeenNthCalledWith(4, {
type: ActionTypes.TOGGLE_WINDOW_SIDE_BAR_PANEL, windowId, panelType: 'closed',
});
});
......
......@@ -7,8 +7,10 @@ import WindowSideBarInfoPanel from '../../../src/containers/WindowSideBarInfoPan
function createWrapper(props) {
return shallow(
<CompanionWindow
windowId="abc123"
id="abc123"
windowId="x"
classes={{}}
companionWindow={{}}
position="right"
{...props}
/>,
......@@ -20,7 +22,7 @@ describe('CompanionWindow', () => {
describe('when the panelContent is set to "info"', () => {
it('renders the WindowSideBarInfoPanel', () => {
companionWindow = createWrapper({ panelContent: 'info' });
companionWindow = createWrapper({ content: 'info' });
expect(companionWindow.find(WindowSideBarInfoPanel).length).toBe(1);
});
});
......@@ -33,16 +35,16 @@ describe('CompanionWindow', () => {
});
describe('when the close companion window button is clicked', () => {
it('triggers the closeCompanionWindow prop with the appropriate args', () => {
const closeCompanionWindowEvent = jest.fn();
it('triggers the onCloseClick prop with the appropriate args', () => {
const removeCompanionWindowEvent = jest.fn();
companionWindow = createWrapper({
closeCompanionWindow: closeCompanionWindowEvent,
onCloseClick: removeCompanionWindowEvent,
});
const closeButton = companionWindow.find('WithStyles(IconButton)[aria-label="closeCompanionWindow"]');
closeButton.simulate('click');
expect(closeCompanionWindowEvent).toHaveBeenCalledTimes(1);
expect(closeCompanionWindowEvent).toHaveBeenCalledWith('abc123', null, 'right');
expect(removeCompanionWindowEvent).toHaveBeenCalledTimes(1);
expect(removeCompanionWindowEvent).toHaveBeenCalledWith('x', 'abc123');
});
});
});
......@@ -5,24 +5,34 @@ import CompanionWindow from '../../../src/containers/CompanionWindow';
import WindowSideBar from '../../../src/containers/WindowSideBar';
import WindowViewer from '../../../src/containers/WindowViewer';
/** create wrapper */
function createWrapper(props) {
return shallow(
<WindowMiddleContent
companionWindowIds={['cw1', 'cw-2']}
window={{ id: 'window-1' }}
manifest={{}}
{...props}
/>,
);
}
describe('WindowMiddleContent', () => {
let wrapper;
let manifest;
it('should render outer element', () => {
wrapper = shallow(<WindowMiddleContent window={window} />);
const wrapper = createWrapper();
expect(wrapper.find('.mirador-window-middle-content')).toHaveLength(1);
});
it('should render <CompanionWindow>', () => {
wrapper = shallow(<WindowMiddleContent window={window} />);
expect(wrapper.find(CompanionWindow)).toHaveLength(1);
it('should render all <CompanionWindow> components', () => {
const wrapper = createWrapper();
expect(wrapper.find(CompanionWindow)).toHaveLength(2);
});
it('should render <WindowSideBar>', () => {
wrapper = shallow(<WindowMiddleContent window={window} />);
const wrapper = createWrapper();
expect(wrapper.find(WindowSideBar)).toHaveLength(1);
});
it('should render <WindowViewer> if manifest is present', () => {
manifest = { id: 456, isFetching: false };
wrapper = shallow(<WindowMiddleContent window={window} manifest={manifest} />);
const manifest = { id: 456, isFetching: false };
const wrapper = createWrapper({ manifest });
expect(wrapper.find(WindowViewer)).toHaveLength(1);
});
});
......@@ -20,7 +20,7 @@ function createWrapper(props) {
windowId="xyz"
classes={{}}
t={str => str}
removeWindow={() => {}}
closeWindow={() => {}}
toggleWindowSideBar={() => {}}
{...props}
/>,
......@@ -67,8 +67,8 @@ describe('WindowTopBar', () => {
});
it('passes correct props to <Button/>', () => {
const removeWindow = jest.fn();
const wrapper = createWrapper({ removeWindow });
expect(wrapper.find(IconButton).last().props().onClick).toBe(removeWindow);
const closeWindow = jest.fn();
const wrapper = createWrapper({ closeWindow });
expect(wrapper.find(IconButton).last().props().onClick).toBe(closeWindow);
});
});
import { companionWindowsReducer } from '../../../src/state/reducers/companionWindows';
import ActionTypes from '../../../src/state/actions/action-types';
describe('companionWindowsReducer', () => {
describe('ADD_COMPANION_WINDOW', () => {
it('adds a new companion window', () => {
const action = {
type: ActionTypes.ADD_COMPANION_WINDOW,
id: 'abc123',
payload: { content: 'info', position: 'right' },
};
const beforeState = {};
const expectedState = {
abc123: {
position: 'right',
content: 'info',
},
};
expect(companionWindowsReducer(beforeState, action)).toEqual(expectedState);
});
});
describe('UPDATE_COMPANION_WINDOW', () => {
it('updates an existing companion window', () => {
const action = {
type: ActionTypes.UPDATE_COMPANION_WINDOW,
id: 'abc123',
payload: { content: 'canvases', foo: 'bar' },
};
const beforeState = {
abc123: {
position: 'right',
content: 'info',
},
};
const expectedState = {
abc123: {
position: 'right',
content: 'canvases',
foo: 'bar',
},
};
expect(companionWindowsReducer(beforeState, action)).toEqual(expectedState);
});
});
describe('REMOVE_COMPANION_WINDOW', () => {
it('should remove a companion window', () => {
const action = {
type: ActionTypes.REMOVE_COMPANION_WINDOW,
id: 'abc123',
};
const beforeState = {
abc123: {
position: 'right',
content: 'info',
},
};
const expectedState = {};
expect(companionWindowsReducer(beforeState, action)).toEqual(expectedState);
});
});
});
......@@ -121,42 +121,6 @@ describe('windows reducer', () => {
});
});
describe('SET_WINDOW_COMPANION_WINDOW', () => {
it('sets the given type under the given position when no companion window exists', () => {
const action = {
type: ActionTypes.SET_WINDOW_COMPANION_WINDOW,
windowId: 'abc123',
position: 'right',
panelType: 'info',
};
const before = {
abc123: {},
};
const after = {
abc123: { companionWindows: { right: 'info' } },
};
expect(windowsReducer(before, action)).toEqual(after);
});
it('overwrites the given position and sets the new type when a companion window in the same position exists', () => {
const action = {
type: ActionTypes.SET_WINDOW_COMPANION_WINDOW,
windowId: 'abc123',
position: 'right',
panelType: 'info',
};
const before = {
abc123: { companionWindows: { right: 'canvas_navigation' } },
};
const after = {
abc123: { companionWindows: { right: 'info' } },
};
expect(windowsReducer(before, action)).toEqual(after);
});
});
it('should handle NEXT_CANVAS', () => {
expect(windowsReducer({
abc123: {
......@@ -230,4 +194,28 @@ describe('windows reducer', () => {
},
});
});
describe('UPDATE_WINDOW', () => {
it('updates an existing window', () => {
const action = {
type: ActionTypes.UPDATE_WINDOW,
id: 'abc123',
payload: { foo: 11, baz: 33 },
};
const beforeState = {
abc123: {
foo: 1,
bar: 2,
},
};
const expectedState = {
abc123: {
foo: 11,
bar: 2,
baz: 33,
},
};
expect(windowsReducer(beforeState, action)).toEqual(expectedState);
});
});
});
......@@ -323,17 +323,20 @@ describe('getIdAndLabelOfCanvases', () => {
describe('getCompanionWindowForPosition', () => {
const state = {
windows: { a: { companionWindows: { right: 'info' } } },
companionWindows: {
abc: { id: 'abc', windowId: 'a', position: 'right' },
xyz: { id: 'xyz', windowId: 'b', position: 'bottom' },
},
};
it('the companion window type based on the given position', () => {
const received = getCompanionWindowForPosition(state, 'a', 'right');
expect(received).toEqual('info');
expect(received.id).toEqual('abc');
});
it('returns undefined if the given window does not exist', () => {
const received = getCompanionWindowForPosition(state, 'b', 'right');
const received = getCompanionWindowForPosition(state, 'c', 'right');
expect(received).toBeUndefined();
});
......
......@@ -36,6 +36,7 @@
"deepmerge": "^3.1.0",
"dompurify": "^1.0.9",
"i18next": "^14.0.1",
"immutable": "^4.0.0-rc.12",
"intersection-observer": "^0.5.1",
"lodash": "^4.17.11",
"manifesto.js": "^3.0.9",
......
......@@ -17,8 +17,9 @@ class CompanionWindow extends Component {
* @return React Component
*/
activePanelComponent() {
const { windowId, panelContent } = this.props;
switch (panelContent) {
const { content, windowId } = this.props;
switch (content) {
case 'info':
return <WindowSideBarInfoPanel windowId={windowId} />;
case 'canvas_navigation':
......@@ -34,8 +35,9 @@ class CompanionWindow extends Component {
*/
render() {
const {
classes, closeCompanionWindow, isDisplayed, position, t, windowId,
classes, id, onCloseClick, isDisplayed, position, t, windowId,
} = this.props;
return (
<Paper
className={[classes.root, ns(`companion-window-${position}`)].join(' ')}
......@@ -46,7 +48,7 @@ class CompanionWindow extends Component {
<IconButton
aria-label={t('closeCompanionWindow')}
className={classes.closeButton}
onClick={() => { closeCompanionWindow(windowId, null, position); }}
onClick={() => { onCloseClick(windowId, id); }}
>
<CloseIcon />
</IconButton>
......@@ -57,18 +59,20 @@ class CompanionWindow extends Component {
CompanionWindow.propTypes = {
classes: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types,
closeCompanionWindow: PropTypes.func,
content: PropTypes.string,
id: PropTypes.string.isRequired,
onCloseClick: PropTypes.func,
position: PropTypes.string,
isDisplayed: PropTypes.bool,
panelContent: PropTypes.string,
position: PropTypes.string.isRequired,
t: PropTypes.func,
windowId: PropTypes.string.isRequired,
};
CompanionWindow.defaultProps = {
closeCompanionWindow: () => {},
panelContent: null,
content: null,
onCloseClick: () => {},
isDisplayed: false,
position: null,
t: key => key,
};
......
......@@ -32,18 +32,19 @@ class WindowMiddleContent extends Component {
* Render the component
*/
render() {
const { window } = this.props;
const { companionWindowIds, window } = this.props;
return (
<div className={ns('window-middle-content')}>
<WindowSideBar windowId={window.id} />
{this.renderViewer()}
<CompanionWindow windowId={window.id} position="right" />
{ companionWindowIds.map(id => <CompanionWindow key={id} id={id} windowId={window.id} />) }
</div>
);
}
}
WindowMiddleContent.propTypes = {
companionWindowIds: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
window: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
manifest: PropTypes.object, // eslint-disable-line react/forbid-prop-types
};
......
......@@ -24,7 +24,7 @@ class WindowTopBar extends Component {
*/
render() {
const {
removeWindow, windowId, classes, toggleWindowSideBar, t, manifestTitle,
closeWindow, windowId, classes, toggleWindowSideBar, t, manifestTitle,
} = this.props;
return (
<AppBar position="relative">
......@@ -46,7 +46,7 @@ class WindowTopBar extends Component {
color="inherit"
className={ns('window-close')}
aria-label={t('closeWindow')}
onClick={removeWindow}
onClick={closeWindow}
>
<CloseIcon />
</IconButton>
......@@ -58,7 +58,7 @@ class WindowTopBar extends Component {
WindowTopBar.propTypes = {
manifestTitle: PropTypes.string,
removeWindow: PropTypes.func.isRequired,
closeWindow: PropTypes.func.isRequired,
windowId: PropTypes.string.isRequired,
classes: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
toggleWindowSideBar: PropTypes.func.isRequired,
......
......@@ -3,7 +3,6 @@ import { connect } from 'react-redux';
import { withNamespaces } from 'react-i18next';
import * as actions from '../state/actions';
import miradorWithPlugins from '../lib/miradorWithPlugins';
import { getCompanionWindowForPosition } from '../state/selectors';
import CompanionWindow from '../components/CompanionWindow';
/**
......@@ -11,13 +10,14 @@ import CompanionWindow from '../components/CompanionWindow';
* @memberof CompanionWindow
* @private
*/
const mapStateToProps = (state, { windowId, position }) => {
const companionWindowForPosition = getCompanionWindowForPosition(state, windowId, position);
const mapStateToProps = (state, { id }) => {
const companionWindow = state.companionWindows[id];
return {
isDisplayed: (companionWindowForPosition
&& companionWindowForPosition.length > 0),
panelContent: companionWindowForPosition,
...companionWindow,
isDisplayed: (companionWindow
&& companionWindow.content
&& companionWindow.content.length > 0),
};
};
......@@ -27,7 +27,7 @@ const mapStateToProps = (state, { windowId, position }) => {
* @private
*/
const mapDispatchToProps = {
closeCompanionWindow: actions.setWindowCompanionWindow,
onCloseClick: actions.closeCompanionWindow,
};
const enhance = compose(
......
import { compose } from 'redux';
import { connect } from 'react-redux';
import { getCompantionWindowIds } from '../state/selectors';
import miradorWithPlugins from '../lib/miradorWithPlugins';
import WindowMiddleContent from '../components/WindowMiddleContent';
/** */
const mapStateToProps = (state, { window }) => ({
companionWindowIds: getCompantionWindowIds(state, window.id),
});
const enhance = compose(
connect(null, null),
connect(mapStateToProps, null),
miradorWithPlugins,
// further HOC go here
);
......
......@@ -17,7 +17,7 @@ const mapStateToProps = (state, { windowId }) => ({
* @private
*/
const mapDispatchToProps = (dispatch, { windowId }) => ({
removeWindow: () => dispatch(actions.removeWindow(windowId)),
closeWindow: () => dispatch(actions.closeWindow(windowId)),
toggleWindowSideBar: () => dispatch(actions.toggleWindowSideBar(windowId)),
});
......
const ActionTypes = {
ADD_COMPANION_WINDOW: 'ADD_COMPANION_WINDOW',
UPDATE_COMPANION_WINDOW: 'UPDATE_COMPANION_WINDOW',
REMOVE_COMPANION_WINDOW: 'REMOVE_COMPANION_WINDOW',
UPDATE_WINDOW: 'UPDATE_WINDOW',
FOCUS_WINDOW: 'FOCUS_WINDOW',
SET_WORKSPACE_FULLSCREEN: 'SET_WORKSPACE_FULLSCREEN',
ADD_MANIFEST: 'ADD_MANIFEST',
......@@ -15,7 +20,6 @@ const ActionTypes = {
SET_WINDOW_THUMBNAIL_POSITION: 'SET_WINDOW_THUMBNAIL_POSITION',
SET_WINDOW_VIEW_TYPE: 'SET_WINDOW_VIEW_TYPE',
SET_WORKSPACE_ADD_VISIBILITY: 'SET_WORKSPACE_ADD_VISIBILITY',
SET_WINDOW_COMPANION_WINDOW: 'SET_WINDOW_COMPANION_WINDOW',
TOGGLE_WINDOW_SIDE_BAR: 'TOGGLE_WINDOW_SIDE_BAR',
TOGGLE_WINDOW_SIDE_BAR_PANEL: 'TOGGLE_WINDOW_SIDE_BAR_PANEL',
TOGGLE_ZOOM_CONTROLS: 'TOGGLE_ZOOM_CONTROLS',
......
import uuid from 'uuid/v4';
import ActionTypes from './action-types';
const defaultProps = {
content: null,
position: null,
};
/** */
export function addCompanionWindow(payload, defaults = defaultProps) {
return {
type: ActionTypes.ADD_COMPANION_WINDOW,
id: `cw-${uuid()}`,
payload: { ...defaults, ...payload },
};
}
/** */
export function updateCompanionWindow(id, payload) {
return { type: ActionTypes.UPDATE_COMPANION_WINDOW, id, payload };
}
/** */
export function removeCompanionWindow(id) {
return { type: ActionTypes.REMOVE_COMPANION_WINDOW, id };
}
......@@ -2,6 +2,7 @@
* Action Creators for Mirador
* @namespace ActionCreators
*/
export * from './companionWindow';
export * from './config';
export * from './window';
export * from './manifest';
......
import uuid from 'uuid/v4';
import ActionTypes from './action-types';
import { addCompanionWindow, removeCompanionWindow } from './companionWindow';
/**
* focusWindow - action creator
......@@ -26,12 +27,18 @@ export function addWindow(options) {
rangeId: null,
thumbnailNavigationPosition: 'bottom', // bottom by default in settings.js
xywh: [0, 0, 400, 400],
companionWindowIds: [],
rotation: null,
view: 'single',
};
return { type: ActionTypes.ADD_WINDOW, window: { ...defaultOptions, ...options } };
}
/** */
export function updateWindow(id, payload) {
return { type: ActionTypes.UPDATE_WINDOW, id, payload };
}
/**
* removeWindow - action creator
*
......@@ -52,25 +59,6 @@ export function toggleWindowSideBar(windowId) {
return { type: ActionTypes.TOGGLE_WINDOW_SIDE_BAR, windowId };
}
/**
* setWindowCompanionWindow - action creator
*
* @param {String} windowId
* @param {String} panelType The type of panel content to be rendered
* in the companion window (e.g. info, canvas_navigation)
* @param {String} position The position of the companion window to
* set content for (e.g. right, bottom)
* @memberof ActionCreators
*/
export function setWindowCompanionWindow(windowId, panelType, position) {
return {
type: ActionTypes.SET_WINDOW_COMPANION_WINDOW,
windowId,
panelType,
position,
};
}
/**
* toggleWindowSideBarPanel - action creator
*
......@@ -93,10 +81,40 @@ export function toggleWindowSideBarPanel(windowId, panelType) {
* @memberof ActionCreators
*/
export function popOutCompanionWindow(windowId, panelType, position) {
return ((dispatch) => {
dispatch(setWindowCompanionWindow(windowId, panelType, position));
return (dispatch, getState) => {
const { companionWindowIds } = getState().windows[windowId];
companionWindowIds.map(id => dispatch(removeCompanionWindow(id)));
const action = dispatch(addCompanionWindow({ content: panelType, position }));
const companionWindowId = action.id;
dispatch(updateWindow(windowId, { companionWindowIds: [companionWindowId] }));
dispatch(toggleWindowSideBarPanel(windowId, 'closed'));
});
};
}
/**
* Clean up state and remove window
*/
export function closeWindow(windowId) {
return (dispatch, getState) => {
const { companionWindowIds } = getState().windows[windowId];
companionWindowIds.map(id => dispatch(removeCompanionWindow(id)));
dispatch(removeWindow(windowId));
};
}
/**
* Close companion window and remove reference from window
*/
export function closeCompanionWindow(windowId, companionWindowId) {
return (dispatch, getState) => {
dispatch(removeCompanionWindow(companionWindowId));
const companionWindowIds = getState().windows[windowId].companionWindowIds
.filter(id => id !== companionWindowId);
dispatch(updateWindow(windowId, { companionWindowIds }));
};
}
/**
......
import {
removeIn, setIn, updateIn, merge,
} from 'immutable';
import ActionTypes from '../actions/action-types';
/** */
export function companionWindowsReducer(state = {}, action) {
switch (action.type) {
case ActionTypes.ADD_COMPANION_WINDOW:
return setIn(state, [action.id], action.payload);
case ActionTypes.UPDATE_COMPANION_WINDOW:
return updateIn(state, [action.id], orig => merge(orig, action.payload));
case ActionTypes.REMOVE_COMPANION_WINDOW:
return removeIn(state, [action.id]);
default:
return state;
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment