Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#9248 - Import/export an application context #9270

Merged
merged 9 commits into from
Jul 10, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/user-guide/application-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ This first step allows to configure the **Name** and the **Window title** of the
!!! note
The **Window title** is the name of the browser window.

### Import/Export context

In general settings, the user can import/export the context in view with all the configurations of the context. The plugins are configured under `context-creator` in the localConfig and are enabled by default.

<img src="../img/application-context/context_import_export.jpg" class="ms-docimage"/>

## Configure Map

To create the context viewer, the map configuration (like the one described [here](exploring-maps.md#exploring-maps) opens so that the admin can set the initial state of the context map.
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 19 additions & 1 deletion web/client/actions/__tests__/contextcreator-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ import {
SET_SELECTED_THEME,
setSelectedTheme,
onToggleCustomVariables,
ON_TOGGLE_CUSTOM_VARIABLES
ON_TOGGLE_CUSTOM_VARIABLES,
onContextExport,
CONTEXT_EXPORT,
onContextImport,
CONTEXT_IMPORT
} from '../contextcreator';

describe('contextcreator actions', () => {
Expand Down Expand Up @@ -177,4 +181,18 @@ describe('contextcreator actions', () => {
expect(retval).toBeTruthy();
expect(retval.type).toBe(ON_TOGGLE_CUSTOM_VARIABLES);
});
it('onContextExport', () => {
const fileName = "test.json";
const retVal = onContextExport(fileName);
expect(retVal).toBeTruthy();
expect(retVal.type).toBe(CONTEXT_EXPORT);
expect(retVal.fileName).toBe(fileName);
});
it('onContextImport', () => {
const file = {name: "test"};
const retVal = onContextImport(file);
expect(retVal).toBeTruthy();
expect(retVal.type).toBe(CONTEXT_IMPORT);
expect(retVal.file).toEqual(file);
});
});
19 changes: 19 additions & 0 deletions web/client/actions/contextcreator.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ export const CONTEXT_TUTORIALS = {
"configure-map": "contextcreator_configuremap_tutorial",
"configure-plugins": "contextcreator_configureplugins_tutorial"
};
export const CONTEXT_EXPORT = 'CONTEXT:EXPORT';
export const CONTEXT_IMPORT = 'CONTEXT:IMPORT';

/**
* Merges initState into context creator state. Meant to be called on ContextCreator component mount
* @param {object} initState state to merge
Expand Down Expand Up @@ -562,3 +565,19 @@ export const showBackToPageConfirmation = (show) => ({
export const onToggleCustomVariables = () => ({
type: ON_TOGGLE_CUSTOM_VARIABLES
});

/**
* Triggers context export
*/
export const onContextExport = (fileName) => ({
type: CONTEXT_EXPORT,
fileName
});

/**
* Triggers context import
*/
export const onContextImport = (file) => ({
type: CONTEXT_IMPORT,
file
});
35 changes: 23 additions & 12 deletions web/client/components/contextcreator/ContextCreator.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,8 @@ export default class ContextCreator extends React.Component {
basicVariables: PropTypes.object,
customVariablesEnabled: PropTypes.bool,
onToggleCustomVariables: PropTypes.func,
enableClickOnStep: PropTypes.bool
enableClickOnStep: PropTypes.bool,
items: PropTypes.array
};

static contextTypes = {
Expand Down Expand Up @@ -244,7 +245,9 @@ export default class ContextCreator extends React.Component {
editingAllowedRoles: []
}
}
}
},
"ContextImport",
"ContextExport"
],
ignoreViewerPlugins: false,
allAvailablePlugins: [],
Expand Down Expand Up @@ -282,11 +285,23 @@ export default class ContextCreator extends React.Component {
}

render() {
const extraToolbarButtons = (stepId) => this.props.tutorials[stepId] ? [{
id: 'show-tutorial',
onClick: () => this.props.onShowTutorial(stepId),
label: 'contextCreator.showTutorial'
}] : [];
const extraToolbarButtons = (stepId) => {
let toolbarButton = this.props.tutorials[stepId] ? [{
id: 'show-tutorial',
onClick: () => this.props.onShowTutorial(stepId),
label: 'contextCreator.showTutorial'
}] : [];
const importExportButtons = this.props.items?.map(({toolbarBtn} = {}) => toolbarBtn) ?? [];
toolbarButton = toolbarButton.concat(importExportButtons);
if (stepId === 'configure-map') {
toolbarButton = toolbarButton.concat({
id: "map-reload",
onClick: () => this.props.onReloadConfirm(true),
label: 'contextCreator.configureMap.reload'
});
}
return toolbarButton;
};

return (
<Stepper
Expand Down Expand Up @@ -319,11 +334,7 @@ export default class ContextCreator extends React.Component {
}, {
id: 'configure-map',
label: 'contextCreator.configureMap.label',
extraToolbarButtons: [...extraToolbarButtons('configure-map'), {
id: "map-reload",
onClick: () => this.props.onReloadConfirm(true),
label: 'contextCreator.configureMap.reload'
}],
extraToolbarButtons: extraToolbarButtons('configure-map'),
component:
<ConfigureMap
pluginsConfig={this.props.ignoreViewerPlugins ?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,37 @@ describe('ContextCreator component', () => {
});

});
it("test plugin container items in general-settings", () => {
const eng = {
"locale": "en-US",
"messages": {
"aboutLbl": "About"
}
};
const actions = {
onSave: () => { }
};
const allAvailablePlugins = [
{enabled: true, title: 'title', pluginConfig: {cfg: {}}},
{enabled: false, title: 'title', pluginConfig: {cfg: {}}}
];
ReactDOM.render(
<Localized messages={eng.messages} locale="en-US">
<Provider store={store}>
<ContextCreator
isCfgValidated
allAvailablePlugins={allAvailablePlugins}
curStepId="general-settings"
onSave={actions.onSave}
items={[{
toolbarBtn: {
component: () => <button>ITEM1</button>
}
}]}
/>
</Provider>
</Localized>, document.getElementById("container"));
const itemBtn = document.querySelectorAll('.footer-button-toolbar-extra button')[1];
expect(itemBtn.innerText).toBe('ITEM1');
});
});
13 changes: 8 additions & 5 deletions web/client/components/misc/Stepper.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,14 @@ export default ({
<div className="ms2-stepper">
<div className="footer-button-toolbar-div">
<ButtonToolbar className="footer-button-toolbar-extra">
{(steps[curStepIndex].extraToolbarButtons || []).map(({onClick = () => {}, label, id}, idx) =>
<Button key={id || label || idx} bsStyle="primary" bsSize="sm" onClick={onClick}>
<Message msgId={label}/>
</Button>
)}
{(steps[curStepIndex].extraToolbarButtons || []).map((toolbarButton, idx) => {
const {component: Component, onClick = () => {}, label, id} = toolbarButton;
return Component ? <Component/> : (
<Button key={id || label || idx} bsStyle="primary" bsSize="sm" onClick={onClick}>
<Message msgId={label}/>
</Button>
);
})}
</ButtonToolbar>
<ButtonToolbar className="footer-button-toolbar">
<Button
Expand Down
2 changes: 2 additions & 0 deletions web/client/configs/localConfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,8 @@
"backToPageConfirmationMessage": "contextCreator.undo"
}
},
"ContextImport",
"ContextExport",
"Notifications",
{
"name": "FeedbackMask",
Expand Down
124 changes: 122 additions & 2 deletions web/client/epics/__tests__/contextcreator-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import {
saveContextResource,
checkIfContextExists,
editTemplateEpic,
mapViewerLoadEpic
mapViewerLoadEpic,
exportContextEpic,
importContextEpic
} from '../contextcreator';
import {
editPlugin,
Expand All @@ -33,6 +35,8 @@ import {
saveNewContext,
editTemplate,
mapViewerLoad,
onContextExport,
onContextImport,
SET_EDITED_PLUGIN,
SET_EDITED_CFG,
SET_CFG_ERROR,
Expand All @@ -51,7 +55,10 @@ import {
CONTEXT_SAVED,
SET_EDITED_TEMPLATE,
SET_PARSED_TEMPLATE,
SET_FILE_DROP_STATUS
SET_FILE_DROP_STATUS,
SET_RESOURCE,
LOAD_FINISHED,
MAP_VIEWER_LOADED
} from '../../actions/contextcreator';
import {INIT_MAP} from '../../actions/map';
import {LOAD_MAP_CONFIG} from '../../actions/config';
Expand All @@ -61,6 +68,7 @@ import {

import axios from "../../libs/ajax";
import MockAdapter from "axios-mock-adapter";
import {TOGGLE_CONTROL} from "../../actions/controls";

describe('contextcreator epics', () => {
let mockAxios;
Expand Down Expand Up @@ -954,4 +962,116 @@ describe('contextcreator epics', () => {
done();
});
});
it('exportContextEpic, export context with plugins and themes', (done) => {
testEpic(exportContextEpic, 1, onContextExport('file.json'), ([a]) => {
expect(a.type).toEqual(TOGGLE_CONTROL);
expect(a.control).toEqual("export");
done();
}, {
map: {
present: {}
},
contextcreator: {
plugins: [{name: "Search", enabled: true}, {name: "PluginUser", isUserPlugin: true, enabled: true}],
selectedTheme: {
color: "#000"
},
newContext: {
templates: []
},
resource: {
name: "test",
description: "Some context"
},
prefetchedData: {
pluginsConfig: [],
allTemplates: []
}
}
});
});
it('importContextEpic into existing context', (done) => {
const blob = new Blob([JSON.stringify({
windowTitle: "test"
})], {
type: "application/json"
});
const jsonFile = new File([blob], "file.json", {
type: "application/json"
});
const epicResult = (actions) => {
expect(actions.length).toBe(5);
expect(actions[0].type).toBe(SET_RESOURCE);
expect(actions[0].resource).toEqual({data: {windowTitle: "test"}, id: 'context1', name: 'test'});
expect(actions[0].pluginsConfig).toEqual([]);
expect(actions[0].allTemplates).toEqual([]);
expect(actions[1].type).toBe(TOGGLE_CONTROL);
expect(actions[1].control).toBe('import');
expect(actions[2].type).toBe(ENABLE_MANDATORY_PLUGINS);
expect(actions[3].type).toBe(LOAD_FINISHED);
expect(actions[4].type).toBe(MAP_VIEWER_LOADED);
expect(actions[4].status).toBe(false);
done();
};
testEpic(importContextEpic, 5, onContextImport([jsonFile]), epicResult, {
contextcreator: {
resource: {
name: "test",
id: "context1"
},
prefetchedData: {
pluginsConfig: [],
allTemplates: []
}
}
}, done);
});

it('importContextEpic into new context', (done) => {
const blob = new Blob([JSON.stringify({
windowTitle: "test"
})], {
type: "application/json"
});
const jsonFile = new File([blob], "file.json", {
type: "application/json"
});
const epicResult = (actions) => {
expect(actions.length).toBe(5);
expect(actions[0].type).toBe(SET_RESOURCE);
expect(actions[0].resource).toEqual({ data: {windowTitle: "test"}, name: 'test'});
expect(actions[0].pluginsConfig).toEqual([]);
expect(actions[0].allTemplates).toEqual([]);
expect(actions[1].type).toBe(TOGGLE_CONTROL);
expect(actions[1].control).toBe('import');
expect(actions[2].type).toBe(ENABLE_MANDATORY_PLUGINS);
expect(actions[3].type).toBe(LOAD_FINISHED);
expect(actions[4].type).toBe(MAP_VIEWER_LOADED);
expect(actions[4].status).toBe(false);
done();
};
testEpic(importContextEpic, 5, onContextImport([jsonFile]), epicResult, {
contextcreator: {
resource: {
name: "test"
},
prefetchedData: {
pluginsConfig: [],
allTemplates: []
}
}
}, done);
});

it('importContextEpic, throws error if no file data is provided', (done) => {
const startActions = [onContextImport(null)];
const epicResult = actions => {
expect(actions.length).toBe(1);
expect(actions[0].type).toBe(SHOW_NOTIFICATION);
expect(actions[0].level).toBe('error');
expect(typeof(actions[0].title) === 'string').toBeTruthy();
done();
};
testEpic(importContextEpic, 1, startActions, epicResult, {}, done);
});
});
Loading