diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000000..438b774c70 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,96 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: [ "master", "develop" ] + paths-ignore: + - 'tests/jest/fileTransformer.js' + pull_request: + branches: [ "master", "develop" ] + paths-ignore: + - 'tests/jest/fileTransformer.js' + schedule: + - cron: '36 8 * * 0' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: javascript-typescript + build-mode: none + # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 520ef8374d..1ff1283011 100755 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,5 +1,16 @@ # Release Notes +## 2.14.1 + +For a full list of changes see: +https://github.com/oskariorg/oskari-frontend/milestone/52?closed=1 + +Fixed an issue where layers with URL templates couldn't be added with the admin UI. + +Removed postinstall script that did not work properly when dev-mode was not used. It was replaced with a Babel-plugin that can handle Cesium quirks. + +Updated dompurify 2.3.6 -> 2.5.8 + ## 2.14.0 For a full list of changes see: diff --git a/bundles/admin/admin-layereditor/view/ServiceEndPoint/ServiceUrlInputHelper.js b/bundles/admin/admin-layereditor/view/ServiceEndPoint/ServiceUrlInputHelper.js index f648974040..f4f06593ba 100644 --- a/bundles/admin/admin-layereditor/view/ServiceEndPoint/ServiceUrlInputHelper.js +++ b/bundles/admin/admin-layereditor/view/ServiceEndPoint/ServiceUrlInputHelper.js @@ -19,9 +19,7 @@ export const cleanUrl = (url) => { keysToDelete.forEach((key) => urlObj.searchParams.delete(key)); const parts = urlObj.toString().split('://'); - if (parts.length > 1) { - return parts[1]; - } - - return urlObj.toString(); + const retValString = parts.length > 1 ? parts[1] : urlObj.toString(); + const decoded = decodeURIComponent(retValString); + return decoded; }; diff --git a/bundles/admin/admin-layereditor/view/ServiceEndPoint/ServiceUrlInputHelper.test.js b/bundles/admin/admin-layereditor/view/ServiceEndPoint/ServiceUrlInputHelper.test.js index 885e0ff91d..e9684d8a4f 100644 --- a/bundles/admin/admin-layereditor/view/ServiceEndPoint/ServiceUrlInputHelper.test.js +++ b/bundles/admin/admin-layereditor/view/ServiceEndPoint/ServiceUrlInputHelper.test.js @@ -59,5 +59,13 @@ describe('ServiceUrlInputHelper Tests ', () => { const url = 'www.com/'; expect(cleanUrl(url)).toBe(url); }); + + it('should NOT encode URL params', () => { + const url = 'avoin-karttakuva.maanmittauslaitos.fi/kiinteisto-avoin/tiles/wmts/1.0.0/kiinteistojaotus/default/v3/ETRS-TM35FIN/{z}/{y}/{x}.pbf'; + expect(cleanUrl(url)).toBe(url); + + const url2 = 'www.com/?first=1&SECOND=2&thiRd=3'; + expect(cleanUrl(url2)).toBe(url2); + }); }); }); diff --git a/bundles/catalogue/metadata/resources/locale/sv.js b/bundles/catalogue/metadata/resources/locale/sv.js index 9afb137983..0744d2db44 100644 --- a/bundles/catalogue/metadata/resources/locale/sv.js +++ b/bundles/catalogue/metadata/resources/locale/sv.js @@ -10,7 +10,7 @@ Oskari.registerLocalization({ "basic": "Grundläggande information", "inspire": "Inspire metadata", "jhs": "ISO 19115 metadata", - "quality": "Data kvalität", + "quality": "Datakvalitet", "actions": "Handlingar" }, "actions": { diff --git a/bundles/catalogue/metadataflyout/resources/locale/sv.js b/bundles/catalogue/metadataflyout/resources/locale/sv.js index e150c51221..b37f2dda04 100644 --- a/bundles/catalogue/metadataflyout/resources/locale/sv.js +++ b/bundles/catalogue/metadataflyout/resources/locale/sv.js @@ -16,7 +16,7 @@ Oskari.registerLocalization({ "abstract": "Grundläggande information", "inspire": "Inspire metadata", "jhs": "ISO 19115 metadata", - "quality": "Data kvalität", + "quality": "Datakvalitet", "actions": "Handlingar", "xml": "ISO 19139 XML fil", "coverage": { diff --git a/bundles/framework/publisher2/handler/PanelGeneralInfoHandler.js b/bundles/framework/publisher2/handler/PanelGeneralInfoHandler.js new file mode 100644 index 0000000000..675a8516bb --- /dev/null +++ b/bundles/framework/publisher2/handler/PanelGeneralInfoHandler.js @@ -0,0 +1,89 @@ +import { StateHandler, controllerMixin } from 'oskari-ui/util'; +import { PUBLISHER_BUNDLE_ID } from '../view/PublisherSideBarHandler'; + +class UIHandler extends StateHandler { + constructor () { + super(); + this.state = { + name: null, + domain: null, + language: null + }; + } + + init (data) { + const { name, domain, language } = data?.metadata || {}; + this.updateState({ + name: name || null, + domain: domain || null, + language: language || Oskari.getLang() + }); + } + + getValues () { + return { + metadata: { + ...this.getState() + } + }; + }; + + onChange (key, value) { + const { oldState } = this.getState(); + const newState = { + ...oldState + }; + newState[key] = value; + this.updateState({ + ...newState + }); + } + + validate () { + let errors = []; + const { name, domain } = this.state; + errors = errors.concat(this.validateName(name)); + errors = errors.concat(this.validateDomain(domain)); + return errors; + } + + validateName (value) { + const errors = []; + const sanitizedValue = Oskari.util.sanitize(value); + if (!value || !value.trim().length) { + errors.push({ + field: name, + error: Oskari.getMsg(PUBLISHER_BUNDLE_ID, 'BasicView.error.name') + }); + return errors; + } + if (sanitizedValue !== value) { + errors.push({ + field: name, + error: Oskari.getMsg(PUBLISHER_BUNDLE_ID, 'BasicView.error.nameIllegalCharacters') + }); + return errors; + } + return errors; + } + + validateDomain (name, value) { + const errors = []; + if (value && value.indexOf('://') !== -1) { + errors.push({ + field: name, + error: Oskari.getMsg(PUBLISHER_BUNDLE_ID, 'BasicView.error.domainStart') + }); + return errors; + } + return errors; + } +} + +const wrapped = controllerMixin(UIHandler, [ + 'validate', + 'getValues', + 'onChange' +]); + +export { wrapped as PanelGeneralInfoHandler }; diff --git a/bundles/framework/publisher2/handler/PanelMapPreviewHandler.js b/bundles/framework/publisher2/handler/PanelMapPreviewHandler.js new file mode 100644 index 0000000000..8a84e66e21 --- /dev/null +++ b/bundles/framework/publisher2/handler/PanelMapPreviewHandler.js @@ -0,0 +1,160 @@ +import { StateHandler, controllerMixin } from 'oskari-ui/util'; +import { PUBLISHER_BUNDLE_ID } from '../view/PublisherSideBarHandler'; + +export const CUSTOM_MAP_SIZE_ID = 'custom'; +const MAP_SIZE_FILL_ID = 'fill'; +export const MAP_SIZES = [{ + id: MAP_SIZE_FILL_ID, + width: '', + height: '', + selected: true, // default option + valid: true +}, { + id: 'small', + width: 580, + height: 387, + valid: true +}, { + id: 'medium', + width: 700, + height: 600, + valid: true +}, { + id: 'large', + width: 1240, + height: 700, + valid: true +}, { + id: CUSTOM_MAP_SIZE_ID, + valid: true +}]; + +export const CUSTOM_MAP_SIZE_LIMITS = { + minWidth: 30, + minHeight: 20, + maxWidth: 4000, + maxHeight: 2000 +}; + +class UIHandler extends StateHandler { + constructor () { + super(); + this.mapmodule = Oskari.getSandbox().findRegisteredModuleInstance('MainMapModule'); + this.state = { + id: MAP_SIZE_FILL_ID + }; + } + + init (data) { + const selectedId = data?.metadata?.preview || MAP_SIZE_FILL_ID; + const selectedMapSize = MAP_SIZES.find(size => size.id === selectedId); + if (selectedId === CUSTOM_MAP_SIZE_ID) { + selectedMapSize.width = data?.metadata?.size?.width || ''; + selectedMapSize.height = data?.metadata?.size?.height || ''; + } + this.updateMapSize(selectedMapSize); + } + + updateMapSize (mapSize) { + this.updateState({ + ...mapSize + }); + if (mapSize.valid) { + this.adjustDataContainer(); + } + } + + /** + * @method adjustDataContainer + * This horrific thing is what sets the left panel components, container and map size. + */ + adjustDataContainer () { + const selectedSize = this.getSelectedMapSize(); + const size = selectedSize.valid ? selectedSize : this.getActiveMapSize(); + + const mapDiv = this.mapmodule.getMapEl(); + mapDiv.width(size.width || '100%'); + mapDiv.height(size.height || '100%'); + } + + /** + * @private @method getActiveMapSize + * Returns an object containing the active map size. + * This will differ from selected size if selected size is invalid. + * + * @return {Object} size + */ + getActiveMapSize () { + const mapDiv = this.mapmodule.getMapEl(); + return { + width: mapDiv.width(), + height: mapDiv.height() + }; + } + + /** + * Stop panel. + * @method stop + * @public + **/ + stop () { + // restore "fill" as default size setting + this.updateMapSize(MAP_SIZES.find(size => size.id === MAP_SIZE_FILL_ID)); + + window.setTimeout(() => { + // calculate new sizes AFTER the publisher panel has been removed from page + // otherwise the publisher panel that we have while stopping is taking up space + // from the map and the map size is calculated wrong + this.adjustDataContainer(); + }, 200); + } + + getValues () { + const selected = this.getSelectedMapSize(); + const values = { + metadata: { + preview: selected.id + } + }; + + if (!isNaN(parseInt(selected.width)) && !isNaN(parseInt(selected.height))) { + values.metadata.size = { + width: selected.width, + height: selected.height + }; + } + return values; + } + + validate () { + const errors = []; + if (!this.getSelectedMapSize().valid) { + errors.push({ + field: 'size', + error: Oskari.getMsg(PUBLISHER_BUNDLE_ID, 'BasicView.error.size', CUSTOM_MAP_SIZE_LIMITS) + }); + } + return errors; + } + + /** + * @private @method getSelectedMapSize + * Returns an object containing the user seleted/set map size and the corresponding size option + * + * @return {Object} size + */ + getSelectedMapSize () { + return { + ...this.state + }; + } +} + +const wrapped = controllerMixin(UIHandler, [ + 'validate', + 'getValues', + 'getSelectedMapSize', + 'updateMapSize' +]); + +export { wrapped as PanelMapPreviewHandler }; diff --git a/bundles/framework/publisher2/instance.js b/bundles/framework/publisher2/instance.js index 33d73b6f12..2232fb03cd 100755 --- a/bundles/framework/publisher2/instance.js +++ b/bundles/framework/publisher2/instance.js @@ -315,7 +315,9 @@ Oskari.clazz.define('Oskari.mapframework.bundle.publisher2.PublisherBundleInstan // call set enabled before rendering the panels (avoid duplicate "normal map plugins") me.publisher.setEnabled(true); - me.publisher.render(root); + const publisherDiv = jQuery('
'); + root.prepend(publisherDiv); + me.publisher.render(publisherDiv); } else { Oskari.setLang(me.oskariLang); if (me.publisher) { diff --git a/bundles/framework/publisher2/resources/locale/en.js b/bundles/framework/publisher2/resources/locale/en.js index 95fa5c84c8..6f146c38b0 100755 --- a/bundles/framework/publisher2/resources/locale/en.js +++ b/bundles/framework/publisher2/resources/locale/en.js @@ -241,7 +241,7 @@ Oskari.registerLocalization( "size": "The map size is invalid. The width should be from {minWidth} to {maxWidth} pixels and the height from {minHeight} to {maxHeight} pixels.", "domain": "The website is required. Please type an address and try again.", "domainStart": "The website is invalid. Please type an address without http or www prefixes and try again.", - "name": "The map name is required. Plese type a name and try again.", + "name": "The map name is required. Please type a name and try again.", "nohelp": "The user guide is not available.", "saveFailed": "The embedded map could not be saved.", "nameIllegalCharacters": "The map name contains illegal characters (e.g. html-tags). Please correct the name and try again.", diff --git a/bundles/framework/publisher2/view/CollapseContent.jsx b/bundles/framework/publisher2/view/CollapseContent.jsx new file mode 100644 index 0000000000..b0eccfcff9 --- /dev/null +++ b/bundles/framework/publisher2/view/CollapseContent.jsx @@ -0,0 +1,16 @@ +import React, { useEffect, useState } from 'react'; +import { Collapse } from 'oskari-ui'; +import { PropTypes } from 'prop-types'; + +export const CollapseContent = ({ controller }) => { + const [items, setItems] = useState(controller.getCollapseItems()); + useEffect(() => { + controller.addStateListener(() => { setItems(controller.getCollapseItems()); }); + }, []); + + return ; +}; + +CollapseContent.propTypes = { + controller: PropTypes.object +}; diff --git a/bundles/framework/publisher2/view/PanelGeneralInfo.js b/bundles/framework/publisher2/view/PanelGeneralInfo.js deleted file mode 100755 index cfe78a9947..0000000000 --- a/bundles/framework/publisher2/view/PanelGeneralInfo.js +++ /dev/null @@ -1,178 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import { GeneralInfoForm } from './form/GeneralInfoForm'; -import { ThemeProvider } from 'oskari-ui/util'; - -/** - * @class Oskari.mapframework.bundle.publisher2.view.PanelGeneralInfo - * - * Represents the basic info (name, domain, language) view for the publisher - * as an Oskari.userinterface.component.AccordionPanel - */ -Oskari.clazz.define('Oskari.mapframework.bundle.publisher2.view.PanelGeneralInfo', - - /** - * @method create called automatically on construction - * @static - * @param {Object} sandbox - * @param {Object} localization - * publisher localization data - */ - function (sandbox, localization) { - this.loc = localization; - this.sandbox = sandbox; - this.fields = { - name: { - label: localization.name.label, - placeholder: localization.name.placeholder, - helptags: 'portti,help,publisher,name', - tooltip: localization.name.tooltip, - value: null - }, - domain: { - label: localization.domain.label, - placeholder: localization.domain.placeholder, - helptags: 'portti,help,publisher,domain', - tooltip: localization.domain.tooltip, - value: null - }, - language: { - value: null - } - }; - this.panel = null; - }, { - /** - * Creates the set of Oskari.userinterface.component.FormInput to be shown on the panel and - * sets up validation etc. Prepopulates the form fields if pData parameter is given. - * - * @method init - * @param {Object} pData initial data - */ - init: function (pData) { - const me = this; - let selectedLang = Oskari.getLang(); - - me.fields.name.validator = function (value) { - const name = 'name'; - const errors = []; - const sanitizedValue = Oskari.util.sanitize(value); - if (!value || !value.trim().length) { - errors.push({ - field: name, - error: me.loc.error.name - }); - return errors; - } - if (sanitizedValue !== value) { - errors.push({ - field: name, - error: me.loc.error.nameIllegalCharacters - }); - return errors; - } - return errors; - }; - - me.fields.domain.validator = function (value) { - const name = 'domain'; - const errors = []; - if (value && value.indexOf('://') !== -1) { - errors.push({ - field: name, - error: me.loc.error.domainStart - }); - return errors; - } - return errors; - }; - - if (pData.metadata) { - // set initial values - me.fields.domain.value = pData.metadata.domain; - me.fields.name.value = pData.metadata.name; - if (pData.metadata.language) { - // if we get data as param -> use lang from it, otherwise use Oskari.getLang() - selectedLang = pData.metadata.language; - } - } - me.fields.language.value = selectedLang; - }, - onChange: function (key, value) { - this.fields[key].value = value; - }, - /** - * Returns the UI panel and populates it with the data that we want to show the user. - * - * @method getPanel - * @return {Oskari.userinterface.component.AccordionPanel} - */ - getPanel: function () { - if (this.panel) { - return this.panel; - } - const panel = Oskari.clazz.create('Oskari.userinterface.component.AccordionPanel'); - const contentPanel = panel.getContainer(); - - ReactDOM.render( - - this.onChange(key, value)} data={this.fields} /> - , - contentPanel[0] - ); - - panel.setTitle(this.loc.domain.title); - this.panel = panel; - return panel; - }, - /** - * Returns the selections the user has done with the form inputs. - * { - * domain : , - * name : , - * language : - * } - * - * @method getValues - * @return {Object} - */ - getValues: function () { - var values = { - metadata: {} - }, - fkey, - data; - - for (fkey in this.fields) { - if (this.fields.hasOwnProperty(fkey)) { - data = this.fields[fkey]; - values.metadata[fkey] = data.value; - } - } - return values; - }, - - /** - * Returns any errors found in validation or an empty - * array if valid. Error object format is defined in Oskari.userinterface.component.FormInput - * validate() function. - * - * @method validate - * @return {Object[]} - */ - validate: function () { - var errors = [], - fkey, - data; - - for (fkey in this.fields) { - if (this.fields.hasOwnProperty(fkey)) { - data = this.fields[fkey]; - if (data.validator) { - errors = errors.concat(data.validator(data.value)); - } - } - } - return errors; - } - }); diff --git a/bundles/framework/publisher2/view/PanelMapPreview.js b/bundles/framework/publisher2/view/PanelMapPreview.js deleted file mode 100755 index cc22f875a7..0000000000 --- a/bundles/framework/publisher2/view/PanelMapPreview.js +++ /dev/null @@ -1,280 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import { CUSTOM_MAP_SIZE_ID, MapPreviewForm, MapPreviewTooltip } from './form/MapPreviewForm'; -import { ThemeProvider } from 'oskari-ui/util'; - -const MAP_SIZE_FILL_ID = 'fill'; -const MAP_SIZES = [{ - id: MAP_SIZE_FILL_ID, - width: '', - height: '', - selected: true, // default option - valid: true -}, { - id: 'small', - width: 580, - height: 387, - valid: true -}, { - id: 'medium', - width: 700, - height: 600, - valid: true -}, { - id: 'large', - width: 1240, - height: 700, - valid: true -}, { - id: CUSTOM_MAP_SIZE_ID, - valid: true -}]; - -export const CUSTOM_MAP_SIZE_LIMITS = { - minWidth: 30, - minHeight: 20, - maxWidth: 4000, - maxHeight: 2000 -}; - -/** - * @class Oskari.mapframework.bundle.publisher2.view.PanelMapPreview - * - * Represents the basic info (name, domain, language) view for the publisher - * as an Oskari.userinterface.component.AccordionPanel - */ -Oskari.clazz.define('Oskari.mapframework.bundle.publisher2.view.PanelMapPreview', - - /** - * @method create called automatically on construction - * @static - * @param {Object} sandbox - * @param {Object} mapmodule - * @param {Object} localization - * publisher localization data - * @param {Oskari.mapframework.bundle.publisher2.insatnce} instance the instance - */ - function (sandbox, mapmodule, localization, instance, tools) { - this.sandbox = sandbox; - this.mapmodule = mapmodule; - this.loc = localization; - this.instance = instance; - this.tools = tools; - this.selectedMapSize = null; - this.sizeLimits = CUSTOM_MAP_SIZE_LIMITS; - }, { - /** - * @method onEvent - * @param {Oskari.mapframework.event.Event} event a Oskari event object - * Event is handled forwarded to correct #eventHandlers if found or discarded if not. - */ - onEvent: function (event) { - const handler = this.eventHandlers[event.getName()]; - if (!handler) { - return; - } - return handler.apply(this, [event]); - }, - /** - * @property {Object} eventHandlers - * @static - */ - eventHandlers: { - MapSizeChangedEvent: function () { - // update map / container size but prevent a new mapsizechanged request from being sent - this.updateMapSize(); - } - }, - getName: function () { - return 'Oskari.mapframework.bundle.publisher2.view.PanelMapPreview'; - }, - /** - * @public @method updateMapSize - * Adjusts the map size according to publisher selection - * - */ - updateMapSize: function () { - if (!this.panel) { - return; - } - this._adjustDataContainer(); - }, - - /** - * @private @method _getActiveMapSize - * Returns an object containing the active map size. - * This will differ from selected size if selected size is invalid. - * - * @return {Object} size - */ - _getActiveMapSize: function () { - const mapDiv = this.mapmodule.getMapEl(); - return { - width: mapDiv.width(), - height: mapDiv.height() - }; - }, - /** - * @private @method _adjustDataContainer - * This horrific thing is what sets the left panel components, container and map size. - */ - _adjustDataContainer: function () { - const selectedSize = this._getSelectedMapSize(); - const size = selectedSize.valid ? selectedSize : this._getActiveMapSize(); - const mapDiv = this.mapmodule.getMapEl(); - mapDiv.width(size.width || '100%'); - mapDiv.height(size.height || '100%'); - }, - - /** - * @private @method _getSelectedMapSize - * Returns an object containing the user seleted/set map size and the corresponding size option - * - * @return {Object} size - */ - _getSelectedMapSize: function () { - return this.selectedMapSize; - }, - /** - * Creates the set of Oskari.userinterface.component.FormInput to be shown on the panel and - * sets up validation etc. Prepopulates the form fields if pData parameter is given. - * - * @method init - * @param {Object} pData initial data - */ - init: function (pData) { - this.populatePanel(pData); - this._registerEventHandlers(); - }, - _registerEventHandlers: function () { - var me = this; - for (var p in me.eventHandlers) { - if (me.eventHandlers.hasOwnProperty(p)) { - me.sandbox.registerForEventByName(me, p); - } - } - }, - _unregisterEventHandlers: function () { - for (const p in this.eventHandlers) { - if (this.eventHandlers.hasOwnProperty(p)) { - this.sandbox.unregisterFromEventByName(this, p); - } - } - }, - /** - * Returns the UI panel and populates it with the data that we want to show the user. - * - * @method getPanel - * @return {Oskari.userinterface.component.AccordionPanel} - */ - getPanel: function () { - if (!this.panel) { - this.populatePanel(); - } - - return this.panel; - }, - - /** - * Populate the actual panel content. - * - * @param data When modifying an existing map get initial size selection from this. - * @method populatePanel - * @return {Oskari.userinterface.component.AccordionPanel} - */ - populatePanel: function (data) { - const panel = Oskari.clazz.create('Oskari.userinterface.component.AccordionPanel'); - const tooltipDiv = document.createElement('div'); - ReactDOM.render(, tooltipDiv); - panel.getHeader().append(tooltipDiv); - - const contentPanel = panel.getContainer(); - const initialSelection = data && data.metadata - ? { id: data.metadata.preview, width: data.metadata.size?.width || null, height: data.metadata.size?.height || null } - : null; - ReactDOM.render( - - { this.mapSizeSelectionChanged(value); }} - mapSizeOptions={MAP_SIZES} - initialSelection={initialSelection}/> - , - contentPanel[0] - ); - - panel.setTitle(this.loc.size.label); - this.panel = panel; - return panel; - }, - - mapSizeSelectionChanged: function (mapSize) { - this.selectedMapSize = mapSize; - if (this.selectedMapSize.valid) { - this.updateMapSize(); - } - }, - /** - * Returns the selections the user has done with the form inputs. - * { - * domain : , - * name : , - * language : - * } - * - * @method getValues - * @return {Object} - */ - getValues: function () { - const selected = this._getSelectedMapSize(); - const values = { - metadata: { - preview: selected.id - } - }; - - if (!isNaN(parseInt(selected.width)) && !isNaN(parseInt(selected.height))) { - values.metadata.size = { - width: selected.width, - height: selected.height - }; - } - return values; - }, - validate: function () { - const errors = []; - if (!this._getSelectedMapSize().valid) { - errors.push({ - field: 'size', - error: Oskari.getMsg('Publisher2', 'BasicView.error.size', this.sizeLimits) - }); - } - return errors; - }, - - /** - * Stop panel. - * @method stop - * @public - **/ - stop: function () { - // restore "fill" as default size setting - this.mapSizeSelectionChanged(MAP_SIZES.find(size => size.id === MAP_SIZE_FILL_ID)); - this._unregisterEventHandlers(); - - window.setTimeout(() => { - // calculate new sizes AFTER the publisher panel has been removed from page - // otherwise the publisher panel that we have while stopping is taking up space - // from the map and the map size is calculated wrong - this._adjustDataContainer(true); - }, 200); - }, - /** - * Gets the label text for a size option. It changes based on grid visibility. - * - * @method _getSizeLabel - * @private - */ - _getSizeLabel: function (label, option) { - return (label + ' (' + option.width + ' x ' + option.height + 'px)'); - } - }); diff --git a/bundles/framework/publisher2/view/PublisherSideBarHandler.js b/bundles/framework/publisher2/view/PublisherSideBarHandler.js new file mode 100644 index 0000000000..4e2c46bbdf --- /dev/null +++ b/bundles/framework/publisher2/view/PublisherSideBarHandler.js @@ -0,0 +1,177 @@ +import React from 'react'; +import { showModal } from 'oskari-ui/components/window'; +import { ValidationErrorMessage } from './dialog/ValidationErrorMessage'; +import { ReplaceConfirmDialogContent } from './dialog/ReplaceConfirmDialogContent'; +import { StateHandler, controllerMixin } from 'oskari-ui/util'; +import { GeneralInfoForm } from './form/GeneralInfoForm'; +import { PanelGeneralInfoHandler } from '../handler/PanelGeneralInfoHandler'; +import { MAP_SIZES, PanelMapPreviewHandler } from '../handler/PanelMapPreviewHandler'; +import { MapPreviewForm, MapPreviewTooltip } from './form/MapPreviewForm'; +import { mergeValues } from '../util/util'; + +export const PUBLISHER_BUNDLE_ID = 'Publisher2'; +const PANEL_GENERAL_INFO_ID = 'panelGeneralInfo'; +const PANEL_MAPPREVIEW_ID = 'panelMapPreview'; + +class PublisherSidebarUIHandler extends StateHandler { + constructor () { + super(); + this.validationErrorMessageDialog = null; + this.replaceConfirmDialog = null; + + this.generalInfoPanelHandler = new PanelGeneralInfoHandler(); + this.mapPreviewPanelHandler = new PanelMapPreviewHandler(); + + this.state = { + collapseItems: [] + }; + } + + updateGeneralInfoPanel () { + const newCollapseItems = this.getState().collapseItems.map(item => item); + const generalInfoPanel = newCollapseItems.find(item => item.key === PANEL_GENERAL_INFO_ID); + generalInfoPanel.children = this.renderGeneralInfoPanel(); + this.updateState({ + collapseItems: newCollapseItems + }); + } + + renderGeneralInfoPanel () { + return
+ this.generalInfoPanelHandler.getController().onChange(key, value)} + data={this.generalInfoPanelHandler.getState()} + /> +
; + } + + updateMapPreviewPanel () { + const newCollapseItems = this.getState().collapseItems.map(item => item); + const panel = newCollapseItems.find(item => item.key === PANEL_MAPPREVIEW_ID); + panel.children = this.renderMapPreviewPanel(); + this.updateState({ + collapseItems: newCollapseItems + }); + } + + renderMapPreviewPanel () { + return
+ + { this.mapPreviewPanelHandler.getController().updateMapSize(value); }} + mapSizeOptions={MAP_SIZES} + initialSelection={this.mapPreviewPanelHandler.getController().getSelectedMapSize() || null}/> +
; + } + + getCollapseItems () { + const { collapseItems } = this.getState(); + return collapseItems; + } + + init (data) { + /** general info - panel */ + this.generalInfoPanelHandler.init(data); + this.generalInfoPanelHandler.addStateListener(() => this.updateGeneralInfoPanel()); + + /** map preview - panel */ + this.mapPreviewPanelHandler.init(data); + this.mapPreviewPanelHandler.addStateListener(() => this.updateMapPreviewPanel()); + + const collapseItems = []; + collapseItems.push({ + key: PANEL_GENERAL_INFO_ID, + label: Oskari.getMsg('Publisher2', 'BasicView.domain.title'), + children: this.renderGeneralInfoPanel() + }); + + collapseItems.push({ + key: PANEL_MAPPREVIEW_ID, + label: Oskari.getMsg('Publisher2', 'BasicView.size.label'), + children: this.renderMapPreviewPanel() + }); + + this.updateState({ + collapseItems + }); + } + + getValues () { + let returnValue = {}; + returnValue = mergeValues(returnValue, this.generalInfoPanelHandler.getValues()); + returnValue = mergeValues(returnValue, this.mapPreviewPanelHandler.getValues()); + return returnValue; + } + + validate () { + let errors = []; + errors = errors.concat(this.generalInfoPanelHandler.validate()); + errors = errors.concat(this.mapPreviewPanelHandler.validate()); + return errors; + } + + stop () { + // TODO: stop individual panels that need stopping. Maybe put these into some array or smthng + this.mapPreviewPanelHandler.stop(); + } + + /** + * @private @method showValidationErrorMessage + * Takes an error array as defined by Oskari.userinterface.component.FormInput validate() and + * shows the errors on a Oskari.userinterface.component.Popup + * + * @param {Object[]} errors validation error objects to show + * + */ + showValidationErrorMessage (errors = []) { + const content = this.closeValidationErrorMessage()}/>; + this.validationErrorMessageDialog = showModal(Oskari.getMsg('Publisher2', 'BasicView.error.title'), content, () => { + this.validationErrorMessageDialog = null; + }); + }; + + closeValidationErrorMessage () { + if (this.validationErrorMessageDialog) { + this.validationErrorMessageDialog.close(); + this.validationErrorMessageDialog = null; + } + } + + /** + * @private @method showReplaceConfirm + * Shows a confirm dialog for replacing published map + * + * @param {Function} continueCallback function to call if the user confirms + * + */ + showReplaceConfirm (continueCallback) { + const content = { + continueCallback(); + this.closeReplaceConfirm(); + }} + closeCallback={() => this.closeReplaceConfirm()}/>; + + this.replaceConfirmDialog = showModal(Oskari.getMsg('Publisher2', 'BasicView.confirm.replace.title'), content); + } + + closeReplaceConfirm () { + if (this.replaceConfirmDialog) { + this.replaceConfirmDialog.close(); + this.replaceConfirmDialog = null; + } + } +} + +const wrapped = controllerMixin(PublisherSidebarUIHandler, [ + 'showValidationErrorMessage', + 'closeValidationErrorMessage', + 'showReplaceConfirm', + 'closeReplaceConfirm', + 'validate', + 'getValues', + 'getCollapseItems', + 'stop' +]); + +export { wrapped as PublisherSidebarHandler }; diff --git a/bundles/framework/publisher2/view/PublisherSidebar.js b/bundles/framework/publisher2/view/PublisherSidebar.js index bdad775ac9..4b3e127a07 100755 --- a/bundles/framework/publisher2/view/PublisherSidebar.js +++ b/bundles/framework/publisher2/view/PublisherSidebar.js @@ -1,584 +1,444 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { mergeValues } from '../util/util'; -import { Messaging, ThemeProvider } from 'oskari-ui/util'; -import { Header } from 'oskari-ui'; +import { ThemeProvider, LocaleProvider, Messaging } from 'oskari-ui/util'; +import { Header, Button, Message } from 'oskari-ui'; import styled from 'styled-components'; import './PanelReactTools'; - +import { mergeValues } from '../util/util'; +import { PublisherSidebarHandler } from './PublisherSideBarHandler'; +import { ButtonContainer } from './dialog/Styled'; +import { SecondaryButton } from 'oskari-ui/components/buttons'; +import { CollapseContent } from './CollapseContent'; const StyledHeader = styled(Header)` padding: 15px 15px 10px 10px; `; +const CollapseWrapper = styled('div')` + margin: 0.25em; +`; + /** * @class Oskari.mapframework.bundle.publisher2.view.PublisherSidebar * Renders the publishers "publish mode" sidebar view where the user can make * selections regarading the map to publish. */ -Oskari.clazz.define('Oskari.mapframework.bundle.publisher2.view.PublisherSidebar', +class PublisherSidebar { + constructor (instance, localization, data) { + this.instance = instance; + this.localization = localization; + this.data = data; + this.normalMapPlugins = []; + this.progressSpinner = Oskari.clazz.create('Oskari.userinterface.component.ProgressSpinner'); + this.panels = []; + this.handler = new PublisherSidebarHandler(); + this.handler.init(data); + } + + render (container) { + this.mainPanel = container; + const content = + +
+ this.cancel()} + /> + + +
+ + + this.cancel()}/> + + { this.data?.uuid && } + + +
+ + ; + + ReactDOM.render(content, container[0]); + + const accordion = Oskari.clazz.create('Oskari.userinterface.component.Accordion'); + const publisherTools = this.createToolGroupings(accordion); + + // Separate handling for RPC and layers group from other tools + // layers panel is added before other tools + // RPC panel is added after other tools + const rpcTools = publisherTools.groups.rpc; + const layerTools = publisherTools.groups.layers; + // clear rpc/layers groups from others for looping/group so they are not listed twice + delete publisherTools.groups.rpc; + delete publisherTools.groups.layers; + + const mapLayersPanel = this.createMapLayersPanel(layerTools); + mapLayersPanel.getPanel().addClass('t_layers'); + this.panels.push(mapLayersPanel); + accordion.addPanel(mapLayersPanel.getPanel()); + // separate tools that support react from ones that don't + const reactGroups = ['tools', 'data', 'statsgrid']; + const reactGroupsTools = {}; + // create panel for each tool group + Object.keys(publisherTools.groups).forEach(group => { + const tools = publisherTools.groups[group]; + if (reactGroups.includes(group)) { + // panels with react groups handled after this + reactGroupsTools[group] = tools; + return; + } + const toolPanel = Oskari.clazz.create('Oskari.mapframework.bundle.publisher2.view.PanelMapTools', + group, tools, this.instance, this.localization + ); + const hasToolsToShow = toolPanel.init(this.data); + this.panels.push(toolPanel); + if (hasToolsToShow) { + const panel = toolPanel.getPanel(); + panel.addClass('t_tools'); + panel.addClass('t_' + group); + accordion.addPanel(panel); + } + }); + Object.keys(reactGroupsTools).forEach(group => { + const tools = reactGroupsTools[group]; + const toolPanel = Oskari.clazz.create('Oskari.mapframework.bundle.publisher2.view.PanelReactTools', tools, group); + const hasToolsToShow = toolPanel.init(this.data); + this.panels.push(toolPanel); + if (hasToolsToShow) { + const panel = toolPanel.getPanel(); + panel.addClass('t_tools'); + panel.addClass('t_' + group); + accordion.addPanel(panel); + } + }); + + // add RPC panel if there are tools for it + if (rpcTools) { + const rpcPanel = this.createRpcPanel(rpcTools); + rpcPanel.getPanel().addClass('t_rpc'); + // add rpc panel after the other tools + this.panels.push(rpcPanel); + accordion.addPanel(rpcPanel.getPanel()); + } + const toolLayoutPanel = this.createToolLayoutPanel(publisherTools.tools); + toolLayoutPanel.getPanel().addClass('t_toollayout'); + this.panels.push(toolLayoutPanel); + accordion.addPanel(toolLayoutPanel.getPanel()); + + const layoutPanel = this.createLayoutPanel(); + layoutPanel.getPanel().addClass('t_style'); + this.panels.push(layoutPanel); + accordion.addPanel(layoutPanel.getPanel()); + + // -- render to UI and setup buttons -- + const contentDiv = container.find('div#jqueryAccordions'); + accordion.insertTo(contentDiv); + // disable keyboard map moving whenever a text-input is focused element + const inputs = contentDiv.find('input[type=text]'); + const sandbox = this.instance.getSandbox(); + inputs.on('focus', () => sandbox.postRequestByName('DisableMapKeyboardMovementRequest')); + inputs.on('blur', () => sandbox.postRequestByName('EnableMapKeyboardMovementRequest')); + } /** - * @static @method create called automatically on construction - * - * @param {Oskari.mapframework.bundle.publisher2.PublisherBundleInstance} instance - * Reference to component that created this view - * @param {Object} localization - * Localization data in JSON format + * @private @method _createToolGroupings + * Finds classes annotated as 'Oskari.mapframework.publisher.Tool'. + * Determines tool groups from tools and creates tool panels for each group. Returns an object containing a list of panels and their tools as well as a list of + * all tools, even those that aren't displayed in the tools' panels. * + * @return {Object} Containing {Oskari.mapframework.bundle.publisher2.view.PanelMapTools[]} list of panels + * and {Oskari.mapframework.publisher.tool.Tool[]} tools not displayed in panel */ - function (instance, localization, data) { - const me = this; - me.data = data; - me.panels = []; - me.instance = instance; - // basic_publisher needs an extra wrapper-div for styles to work properly - me.template = jQuery( - '
' + - '
' + - '
' + - '
' + - '
' + - '
'); - - me.templates = { - publishedGridTemplate: '
' + createToolGroupings () { + const sandbox = this.instance.getSandbox(); + const mapmodule = sandbox.findRegisteredModuleInstance('MainMapModule'); + const definedTools = [...Oskari.clazz.protocol('Oskari.mapframework.publisher.Tool'), + ...Oskari.clazz.protocol('Oskari.mapframework.publisher.LayerTool') + ]; + + const grouping = {}; + const allTools = []; + // group tools per tool-group + definedTools.forEach(toolname => { + const tool = Oskari.clazz.create(toolname, sandbox, mapmodule, this.localization); + const group = tool.getGroup(); + if (!grouping[group]) { + grouping[group] = []; + } + this.addToolConfig(tool); + grouping[group].push(tool); + allTools.push(tool); + }); + return { + groups: grouping, + tools: allTools }; + } - me.templateButtonsDiv = jQuery('
'); - me.normalMapPlugins = []; - - me.loc = localization; - me.accordion = null; - - me.maplayerPanel = null; - me.mainPanel = null; - - me.latestGFI = null; - - me.progressSpinner = Oskari.clazz.create('Oskari.userinterface.component.ProgressSpinner'); - }, { - /** - * @method render - * Renders view to given DOM element - * @param {jQuery} container reference to DOM element this component will be - * rendered to - */ - render: function (container) { - const content = this.template.clone(); - this.mainPanel = content; - - this.progressSpinner.insertTo(content); - // prepend makes the sidebar go on the left side of the map - // we could use getNavigationDimensions() and check placement from it to append OR prepend, - // but it does work with the navigation even on the right hand side being hidden, - // a new panel appearing on the left hand side and the map moves accordingly - container.prepend(content); - const accordion = Oskari.clazz.create('Oskari.userinterface.component.Accordion'); - this.accordion = accordion; - - const header = content.find('div.header'); - const headerContainer = jQuery('
'); - header.append(headerContainer); - ReactDOM.render( - - this.cancel()} - /> - , - headerContainer[0] - ); + addToolConfig (tool) { + const conf = this.instance.conf || {}; + if (!conf.toolsConfig || !tool.bundleName) { + return; + } + tool.toolConfig = conf.toolsConfig[tool.bundleName]; + } - // -- create panels -- - const genericInfoPanel = this._createGeneralInfoPanel(); - genericInfoPanel.getPanel().addClass('t_generalInfo'); - this.panels.push(genericInfoPanel); - accordion.addPanel(genericInfoPanel.getPanel()); - - const publisherTools = this._createToolGroupings(accordion); - - const mapPreviewPanel = this._createMapPreviewPanel(publisherTools.tools); - mapPreviewPanel.getPanel().addClass('t_size'); - this.panels.push(mapPreviewPanel); - accordion.addPanel(mapPreviewPanel.getPanel()); - - // Separate handling for RPC and layers group from other tools - // layers panel is added before other tools - // RPC panel is added after other tools - const rpcTools = publisherTools.groups.rpc; - const layerTools = publisherTools.groups.layers; - // clear rpc/layers groups from others for looping/group so they are not listed twice - delete publisherTools.groups.rpc; - delete publisherTools.groups.layers; - - const mapLayersPanel = this._createMapLayersPanel(layerTools); - mapLayersPanel.getPanel().addClass('t_layers'); - this.panels.push(mapLayersPanel); - accordion.addPanel(mapLayersPanel.getPanel()); - // separate tools that support react from ones that don't - const reactGroups = ['tools', 'data', 'statsgrid']; - const reactGroupsTools = {}; - // create panel for each tool group - Object.keys(publisherTools.groups).forEach(group => { - const tools = publisherTools.groups[group]; - if (reactGroups.includes(group)) { - // panels with react groups handled after this - reactGroupsTools[group] = tools; - return; - } - const toolPanel = Oskari.clazz.create('Oskari.mapframework.bundle.publisher2.view.PanelMapTools', - group, tools, this.instance, this.loc - ); - const hasToolsToShow = toolPanel.init(this.data); - this.panels.push(toolPanel); - if (hasToolsToShow) { - const panel = toolPanel.getPanel(); - panel.addClass('t_tools'); - panel.addClass('t_' + group); - accordion.addPanel(panel); - } - }); - Object.keys(reactGroupsTools).forEach(group => { - const tools = reactGroupsTools[group]; - const toolPanel = Oskari.clazz.create('Oskari.mapframework.bundle.publisher2.view.PanelReactTools', tools, group); - const hasToolsToShow = toolPanel.init(this.data); - this.panels.push(toolPanel); - if (hasToolsToShow) { - const panel = toolPanel.getPanel(); - panel.addClass('t_tools'); - panel.addClass('t_' + group); - accordion.addPanel(panel); - } - }); + createMapLayersPanel (tools) { + const sandbox = this.instance.getSandbox(); + const mapModule = sandbox.findRegisteredModuleInstance('MainMapModule'); + const form = Oskari.clazz.create('Oskari.mapframework.bundle.publisher2.view.PanelMapLayers', tools, sandbox, mapModule, this.localization, this.instance); + form.init(this.data); + return form; + } - // add RPC panel if there are tools for it - if (rpcTools) { - const rpcPanel = this._createRpcPanel(rpcTools); - rpcPanel.getPanel().addClass('t_rpc'); - // add rpc panel after the other tools - this.panels.push(rpcPanel); - accordion.addPanel(rpcPanel.getPanel()); - } - const toolLayoutPanel = this._createToolLayoutPanel(publisherTools.tools); - toolLayoutPanel.getPanel().addClass('t_toollayout'); - this.panels.push(toolLayoutPanel); - accordion.addPanel(toolLayoutPanel.getPanel()); - - const layoutPanel = this._createLayoutPanel(); - layoutPanel.getPanel().addClass('t_style'); - this.panels.push(layoutPanel); - accordion.addPanel(layoutPanel.getPanel()); - - // -- render to UI and setup buttons -- - const contentDiv = content.find('div.content'); - accordion.insertTo(contentDiv); - contentDiv.append(this._getButtons()); - - // disable keyboard map moving whenever a text-input is focused element - const inputs = this.mainPanel.find('input[type=text]'); - const sandbox = this.instance.getSandbox(); - inputs.on('focus', () => sandbox.postRequestByName('DisableMapKeyboardMovementRequest')); - inputs.on('blur', () => sandbox.postRequestByName('EnableMapKeyboardMovementRequest')); - }, - - /** - * @private @method _createGeneralInfoPanel - * Creates the Location panel of publisher - */ - _createGeneralInfoPanel: function () { - var me = this; - var sandbox = this.instance.getSandbox(); - var form = Oskari.clazz.create( - 'Oskari.mapframework.bundle.publisher2.view.PanelGeneralInfo', - sandbox, me.loc - ); + /** + * @private @method _createToolLayoutPanel + * Creates the tool layout panel of publisher + * @param {Oskari.mapframework.publisher.tool.Tool[]} tools + */ + createToolLayoutPanel (tools) { + const sandbox = this.instance.getSandbox(); + const mapModule = sandbox.findRegisteredModuleInstance('MainMapModule'); + const form = Oskari.clazz.create('Oskari.mapframework.bundle.publisher2.view.PanelToolLayout', + tools, sandbox, mapModule, this.localization, this.instance + ); + + // initialize form (restore data when editing) + form.init(this.data); + return form; + } - // initialize form (restore data when editing) - form.init(me.data); - // open generic info by default - form.getPanel().open(); - return form; - }, - - /** - * @private @method _createMapSizePanel - * Creates the Map Sizes panel of publisher - */ - _createMapPreviewPanel: function (publisherTools) { - const me = this; - const sandbox = this.instance.getSandbox(); - const mapModule = sandbox.findRegisteredModuleInstance('MainMapModule'); - const form = Oskari.clazz.create('Oskari.mapframework.bundle.publisher2.view.PanelMapPreview', - sandbox, mapModule, me.loc, me.instance, publisherTools - ); + /** + * @private @method _createLayoutPanel + * Creates the layout panel of publisher + */ + createLayoutPanel () { + const sandbox = this.instance.getSandbox(); + const mapModule = sandbox.findRegisteredModuleInstance('MainMapModule'); + const form = Oskari.clazz.create('Oskari.mapframework.bundle.publisher2.view.PanelLayout', + sandbox, mapModule, this.localization, this.instance + ); + + // initialize form (restore data when editing) + form.init(this.data); + + return form; + } + + createRpcPanel (tools) { + const sandbox = this.instance.getSandbox(); + const form = Oskari.clazz.create('Oskari.mapframework.bundle.publisher2.view.PanelRpc', + tools, sandbox, this.localization, this.instance + ); + form.init(this.data); + return form; + } - // initialize form (restore data when editing) - form.init(me.data); - return form; - }, - _createMapLayersPanel: function (tools) { - const sandbox = this.instance.getSandbox(); - const mapModule = sandbox.findRegisteredModuleInstance('MainMapModule'); - const form = Oskari.clazz.create('Oskari.mapframework.bundle.publisher2.view.PanelMapLayers', tools, sandbox, mapModule, this.loc, this.instance); - form.init(this.data); - return form; - }, - /** - * @private @method _createToolLayoutPanel - * Creates the tool layout panel of publisher - * @param {Oskari.mapframework.publisher.tool.Tool[]} tools - */ - _createToolLayoutPanel: function (tools) { - var me = this, - sandbox = this.instance.getSandbox(), - mapModule = sandbox.findRegisteredModuleInstance('MainMapModule'), - form = Oskari.clazz.create('Oskari.mapframework.bundle.publisher2.view.PanelToolLayout', - tools, sandbox, mapModule, me.loc, me.instance - ); - - // initialize form (restore data when editing) - form.init(me.data); - return form; - }, - /** - * @private @method _createLayoutPanel - * Creates the layout panel of publisher - */ - _createLayoutPanel: function () { - var me = this, - sandbox = this.instance.getSandbox(), - mapModule = sandbox.findRegisteredModuleInstance('MainMapModule'), - form = Oskari.clazz.create('Oskari.mapframework.bundle.publisher2.view.PanelLayout', - sandbox, mapModule, me.loc, me.instance - ); - - // initialize form (restore data when editing) - form.init(me.data); - - return form; - }, - _createRpcPanel: function (tools) { - const sandbox = this.instance.getSandbox(); - const form = Oskari.clazz.create('Oskari.mapframework.bundle.publisher2.view.PanelRpc', - tools, sandbox, this.loc, this.instance - ); - form.init(this.data); - return form; - }, - - /** - * @private @method _createToolGroupings - * Finds classes annotated as 'Oskari.mapframework.publisher.Tool'. - * Determines tool groups from tools and creates tool panels for each group. Returns an object containing a list of panels and their tools as well as a list of - * all tools, even those that aren't displayed in the tools' panels. - * - * @return {Object} Containing {Oskari.mapframework.bundle.publisher2.view.PanelMapTools[]} list of panels - * and {Oskari.mapframework.publisher.tool.Tool[]} tools not displayed in panel - */ - _createToolGroupings: function () { - const sandbox = this.instance.getSandbox(); - const mapmodule = sandbox.findRegisteredModuleInstance('MainMapModule'); - const definedTools = [...Oskari.clazz.protocol('Oskari.mapframework.publisher.Tool'), - ...Oskari.clazz.protocol('Oskari.mapframework.publisher.LayerTool') - ]; - - const grouping = {}; - const allTools = []; - // group tools per tool-group - definedTools.forEach(toolname => { - const tool = Oskari.clazz.create(toolname, sandbox, mapmodule, this.loc); - var group = tool.getGroup(); - if (!grouping[group]) { - grouping[group] = []; - } - this._addToolConfig(tool); - grouping[group].push(tool); - allTools.push(tool); - }); - return { - groups: grouping, - tools: allTools - }; - }, - _addToolConfig: function (tool) { - var conf = this.instance.conf || {}; - if (!conf.toolsConfig || !tool.bundleName) { - return; - } - tool.toolConfig = conf.toolsConfig[tool.bundleName]; - }, - /** - * Gather selections. - * @method gatherSelections - * @private - */ - gatherSelections: function () { - const sandbox = this.instance.getSandbox(); - let errors = []; - - const mapFullState = sandbox.getStatefulComponents().mapfull.getState(); - let selections = { - configuration: { - mapfull: { - state: mapFullState - } - } - }; + /** + * @method setEnabled + * "Activates" the published map preview when enabled + * and returns to normal mode on disable + * + * @param {Boolean} isEnabled true to enable preview, false to disable + * preview + * + */ + setEnabled (isEnabled) { + if (isEnabled) { + this.enablePreview(); + } else { + this.stopEditorPanels(); + this.disablePreview(); + } + } - this.panels.forEach((panel) => { - if (typeof panel.validate === 'function') { - errors = errors.concat(panel.validate()); + /** + * @private @method _enablePreview + * Modifies the main map to show what the published map would look like + * + * + */ + enablePreview () { + const sandbox = this.instance.sandbox; + const mapModule = sandbox.findRegisteredModuleInstance('MainMapModule'); + Object.values(mapModule.getPluginInstances()) + .filter(plugin => plugin.isShouldStopForPublisher && plugin.isShouldStopForPublisher()) + .forEach(plugin => { + try { + plugin.stopPlugin(sandbox); + mapModule.unregisterPlugin(plugin); + this.normalMapPlugins.push(plugin); + } catch (err) { + Oskari.log('Publisher').error('Enable preview', err); + Messaging.error(this.localization?.error.enablePreview); } - selections = mergeValues(selections, panel.getValues()); }); + } - if (errors.length > 0) { - this._showValidationErrorMessage(errors); - return null; + /** + * @private @method _disablePreview + * Returns the main map from preview to normal state + * + */ + disablePreview () { + const sandbox = this.instance.sandbox; + const mapModule = sandbox.findRegisteredModuleInstance('MainMapModule'); + // resume normal plugins + this.normalMapPlugins.forEach(plugin => { + mapModule.registerPlugin(plugin); + plugin.startPlugin(sandbox); + if (plugin.refresh) { + plugin.refresh(); } - return selections; - }, - - /** - * @private @method _stopEditorPanels - */ - _stopEditorPanels: function () { - this.panels.forEach(function (panel) { - if (typeof panel.stop === 'function') { - panel.stop(); - } - }); - }, - /** - * @method cancel - * Closes publisher without saving - */ - cancel: function () { - this.instance.setPublishMode(false); - }, - /** - * @private @method _getButtons - * Renders publisher buttons to DOM snippet and returns it. - * - * - * @return {jQuery} container with buttons - */ - _getButtons: function () { - const me = this; - const buttonCont = me.templateButtonsDiv.clone(); - // cancel - const cancelBtn = Oskari.clazz.create('Oskari.userinterface.component.buttons.CancelButton'); - cancelBtn.setHandler(function () { - me.cancel(); - }); - cancelBtn.insertTo(buttonCont); - // save - const saveBtn = Oskari.clazz.create('Oskari.userinterface.component.buttons.SaveButton'); - if (!me.data.uuid) { - // only save when not editing - saveBtn.setTitle(me.loc.buttons.save); - saveBtn.setHandler(function () { - const selections = me.gatherSelections(); - if (selections) { - me._stopEditorPanels(); - me._publishMap(selections); - } - }); - saveBtn.insertTo(buttonCont); - return buttonCont; + }); + // reset listing + this.normalMapPlugins = []; + } + + /** + * @private @method _stopEditorPanels + */ + stopEditorPanels () { + this.panels.forEach(function (panel) { + if (typeof panel.stop === 'function') { + panel.stop(); } - // buttons when editing - const save = function () { - const selections = me.gatherSelections(); - if (selections) { - me._stopEditorPanels(); - me._publishMap(selections); + }); + + this.handler.stop(); + } + + /** + * @method cancel + * Closes publisher without saving + */ + cancel () { + this.instance.setPublishMode(false); + } + + confirmReplace () { + this.handler.showReplaceConfirm(() => this.save()); + } + + save () { + const selections = this.gatherSelections(); + if (selections) { + this.stopEditorPanels(); + this.publishMap(selections); + } + } + + saveAsNew () { + if (this.data?.uuid) { + this.data.uuid = null; + delete this.data.uuid; + } + this.save(); + } + + /** + * Gather selections. + * @method gatherSelections + * @private + */ + gatherSelections () { + const sandbox = this.instance.getSandbox(); + let errors = []; + + const mapFullState = sandbox.getStatefulComponents().mapfull.getState(); + let selections = { + configuration: { + mapfull: { + state: mapFullState } - }; - saveBtn.setTitle(me.loc.buttons.saveNew); - saveBtn.setHandler(function () { - // clear the id to save this as a new embedded map - me.data.uuid = null; - delete me.data.uuid; - save(); - }); - saveBtn.insertTo(buttonCont); - - // replace - const replaceBtn = Oskari.clazz.create('Oskari.userinterface.component.Button'); - replaceBtn.setTitle(me.loc.buttons.replace); - replaceBtn.addClass('primary'); - replaceBtn.setHandler(function () { - me._showReplaceConfirm(save); - }); - replaceBtn.insertTo(buttonCont); - return buttonCont; - }, - /** - * @private @method _publishMap - * Sends the gathered map data to the server to save them/publish the map. - * - * @param {Object} selections map data as returned by gatherSelections() - * - */ - _publishMap: function (selections) { - var me = this, - sandbox = me.instance.getSandbox(), - totalWidth = '100%', - totalHeight = '100%', - errorHandler = function () { - me.progressSpinner.stop(); - var dialog = Oskari.clazz.create('Oskari.userinterface.component.Popup'), - okBtn = dialog.createCloseButton(me.loc.buttons.ok); - dialog.show(me.loc.error.title, me.loc.error.saveFailed, [okBtn]); - }; - if (selections.metadata.size) { - totalWidth = selections.metadata.size.width + 'px'; - totalHeight = selections.metadata.size.height + 'px'; } + }; - me.progressSpinner.start(); - - // make the ajax call - jQuery.ajax({ - url: Oskari.urls.getRoute('AppSetup'), - type: 'POST', - dataType: 'json', - data: { - publishedFrom: Oskari.app.getUuid(), - uuid: (me.data && me.data.uuid) ? me.data.uuid : undefined, - pubdata: JSON.stringify(selections) - }, - success: function (response) { - me.progressSpinner.stop(); - if (response.id > 0) { - var event = Oskari.eventBuilder( - 'Publisher.MapPublishedEvent' - )( - response.id, - totalWidth, - totalHeight, - response.lang, - sandbox.createURL(response.url) - ); - - me._stopEditorPanels(); - sandbox.notifyAll(event); - } else { - errorHandler(); - } - }, - error: errorHandler - }); - }, - - /** - * @method setEnabled - * "Activates" the published map preview when enabled - * and returns to normal mode on disable - * - * @param {Boolean} isEnabled true to enable preview, false to disable - * preview - * - */ - setEnabled: function (isEnabled) { - if (isEnabled) { - this._enablePreview(); - } else { - this._stopEditorPanels(); - this._disablePreview(); + this.panels.forEach((panel) => { + if (typeof panel.validate === 'function') { + errors = errors.concat(panel.validate()); } - }, - /** - * @private @method _enablePreview - * Modifies the main map to show what the published map would look like - * - * - */ - _enablePreview: function () { - const sandbox = this.instance.sandbox; - const mapModule = sandbox.findRegisteredModuleInstance('MainMapModule'); - Object.values(mapModule.getPluginInstances()) - .filter(plugin => plugin.isShouldStopForPublisher && plugin.isShouldStopForPublisher()) - .forEach(plugin => { - try { - plugin.stopPlugin(sandbox); - mapModule.unregisterPlugin(plugin); - this.normalMapPlugins.push(plugin); - } catch (err) { - Oskari.log('Publisher').error('Enable preview', err); - Messaging.error(this.loc.error.enablePreview); - } - }); - }, - - /** - * @private @method _disablePreview - * Returns the main map from preview to normal state - * - */ - _disablePreview: function () { - const sandbox = this.instance.sandbox; - var mapModule = sandbox.findRegisteredModuleInstance('MainMapModule'); - // resume normal plugins - this.normalMapPlugins.forEach(plugin => { - mapModule.registerPlugin(plugin); - plugin.startPlugin(sandbox); - if (plugin.refresh) { - plugin.refresh(); - } - }); - // reset listing - this.normalMapPlugins = []; - }, - /** - * @private @method _showValidationErrorMessage - * Takes an error array as defined by Oskari.userinterface.component.FormInput validate() and - * shows the errors on a Oskari.userinterface.component.Popup - * - * @param {Object[]} errors validation error objects to show - * - */ - _showValidationErrorMessage: function (errors = []) { - const dialog = Oskari.clazz.create('Oskari.userinterface.component.Popup'); - const okBtn = dialog.createCloseButton(this.loc.buttons.ok); - const content = jQuery('
    '); - errors.map(err => { - const row = jQuery('
  • '); - row.append(err.error); - return row; - }).forEach(row => content.append(row)); - dialog.makeModal(); - dialog.show(this.loc.error.title, content, [okBtn]); - }, - /** - * @private @method _showReplaceConfirm - * Shows a confirm dialog for replacing published map - * - * @param {Function} continueCallback function to call if the user confirms - * - */ - _showReplaceConfirm: function (continueCallback) { + selections = mergeValues(selections, panel.getValues()); + }); + + errors = errors.concat(this.handler.validate()); + selections = mergeValues(selections, this.handler.getValues()); + + if (errors.length > 0) { + this.handler.showValidationErrorMessage(errors); + return null; + } + return selections; + } + + /** + * @private @method publishMap + * Sends the gathered map data to the server to save them/publish the map. + * + * @param {Object} selections map data as returned by gatherSelections() + * + */ + publishMap (selections) { + const me = this; + const sandbox = this.instance.getSandbox(); + let totalWidth = '100%'; + let totalHeight = '100%'; + const errorHandler = () => { + this.progressSpinner.stop(); const dialog = Oskari.clazz.create('Oskari.userinterface.component.Popup'); - const okBtn = Oskari.clazz.create('Oskari.userinterface.component.Button'); - okBtn.setTitle(this.loc.buttons.replace); - okBtn.addClass('primary'); - okBtn.setHandler(function () { - dialog.close(); - continueCallback(); - }); - const cancelBtn = dialog.createCloseButton(this.loc.buttons.cancel); - dialog.show( - this.loc.confirm.replace.title, - this.loc.confirm.replace.msg, - [cancelBtn, okBtn] - ); - }, - /** - * @method destroy - * Destroys/removes this view from the screen. - */ - destroy: function () { - this.mainPanel.remove(); + const okBtn = dialog.createCloseButton(this.localization.buttons.ok); + dialog.show(this.localization.error.title, this.localization.error.saveFailed, [okBtn]); + }; + + if (selections.metadata.size) { + totalWidth = selections.metadata.size.width + 'px'; + totalHeight = selections.metadata.size.height + 'px'; } - }); + + this.progressSpinner.start(); + + // make the ajax call + jQuery.ajax({ + url: Oskari.urls.getRoute('AppSetup'), + type: 'POST', + dataType: 'json', + data: { + publishedFrom: Oskari.app.getUuid(), + uuid: (this.data && this.data.uuid) ? this.data.uuid : undefined, + pubdata: JSON.stringify(selections) + }, + success: function (response) { + me.progressSpinner.stop(); + if (response.id > 0) { + const event = Oskari.eventBuilder( + 'Publisher.MapPublishedEvent' + )( + response.id, + totalWidth, + totalHeight, + response.lang, + sandbox.createURL(response.url) + ); + + me.stopEditorPanels(); + sandbox.notifyAll(event); + } else { + errorHandler(); + } + }, + error: errorHandler + }); + } + + destroy () { + // TODO: this is still jQueryish. Make it not be. + this.mainPanel.remove(); + } +} + +Oskari.clazz.defineES('Oskari.mapframework.bundle.publisher2.view.PublisherSidebar', + PublisherSidebar +); + +export { PublisherSidebar }; diff --git a/bundles/framework/publisher2/view/dialog/ReplaceConfirmDialogContent.jsx b/bundles/framework/publisher2/view/dialog/ReplaceConfirmDialogContent.jsx new file mode 100644 index 0000000000..3aa12d620f --- /dev/null +++ b/bundles/framework/publisher2/view/dialog/ReplaceConfirmDialogContent.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Message } from 'oskari-ui'; +import { SecondaryButton } from 'oskari-ui/components/buttons'; +import { ButtonContainer, DialogContentContainer } from './Styled'; +import { LocaleProvider } from 'oskari-ui/util'; + +export const ReplaceConfirmDialogContent = ({ okCallback, closeCallback }) => { + return + + + + + + + + ; +}; + +ReplaceConfirmDialogContent.propTypes = { + okCallback: PropTypes.func, + closeCallback: PropTypes.func +}; diff --git a/bundles/framework/publisher2/view/dialog/Styled.js b/bundles/framework/publisher2/view/dialog/Styled.js new file mode 100644 index 0000000000..26e6ba7094 --- /dev/null +++ b/bundles/framework/publisher2/view/dialog/Styled.js @@ -0,0 +1,14 @@ +import styled from 'styled-components'; + +export const ButtonContainer = styled('div')` + margin: 1em 1em 0 1em ; + display: flex; + gap: 0.5em; + justify-content: center; +`; + +export const DialogContentContainer = styled('div')` + margin: 1em; + display: flex; + flex-direction: column; +`; diff --git a/bundles/framework/publisher2/view/dialog/ValidationErrorMessage.jsx b/bundles/framework/publisher2/view/dialog/ValidationErrorMessage.jsx new file mode 100644 index 0000000000..1901362928 --- /dev/null +++ b/bundles/framework/publisher2/view/dialog/ValidationErrorMessage.jsx @@ -0,0 +1,36 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import { PrimaryButton } from 'oskari-ui/components/buttons'; + +const MessageContainer = styled('div')` + margin: 1em; + display: flex; + flex-direction: column; +`; + +const ErrorList = styled('ul')` + padding: 0 1em; +`; +const ButtonContainer = styled('div')` + margin: 1em 0 0 0 ; + display: flex; + justify-content: center; +`; + +export const ValidationErrorMessage = ({ errors, closeCallback }) => { + const listItems = errors.map((err, index) =>
  • {err.error}
  • ); + return + + {listItems} + + + + + ; +}; + +ValidationErrorMessage.propTypes = { + errors: PropTypes.array, + closeCallback: PropTypes.func +}; diff --git a/bundles/framework/publisher2/view/form/GeneralInfoForm.jsx b/bundles/framework/publisher2/view/form/GeneralInfoForm.jsx index bf116130c0..d8df24f53c 100644 --- a/bundles/framework/publisher2/view/form/GeneralInfoForm.jsx +++ b/bundles/framework/publisher2/view/form/GeneralInfoForm.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React from 'react'; import { LabeledInput, Select, Option, Message, Label } from 'oskari-ui'; import styled from 'styled-components'; import { InfoIcon } from 'oskari-ui/components/icons'; @@ -13,14 +13,7 @@ const FieldWithInfo = styled('div')` const BUNDLE_KEY = 'Publisher2'; export const GeneralInfoForm = ({ onChange, data }) => { - const [state, setState] = useState({ - name: data.name.value ? data.name.value : null, - domain: data.domain.value ? data.domain.value : null, - language: data.language.value ? data.language.value : Oskari.getLang() - }); - const languages = Oskari.getSupportedLanguages(); - return (
    @@ -28,16 +21,12 @@ export const GeneralInfoForm = ({ onChange, data }) => { type='text' label={} name='name' - value={state.name} + value={data.name} mandatory={true} onChange={(e) => { - setState({ - ...state, - name: e.target.value - }); onChange('name', e.target.value); }} - placeholder={data.name.placeholder} + placeholder={Oskari.getMsg(BUNDLE_KEY, 'BasicView.name.placeholder')} /> @@ -46,15 +35,11 @@ export const GeneralInfoForm = ({ onChange, data }) => { type='text' label={} name='domain' - value={state.domain} + value={data.domain} onChange={(e) => { - setState({ - ...state, - domain: e.target.value - }); onChange('domain', e.target.value); }} - placeholder={data.domain.placeholder} + placeholder={Oskari.getMsg(BUNDLE_KEY, 'BasicView.domain.placeholder')} /> @@ -64,12 +49,8 @@ export const GeneralInfoForm = ({ onChange, data }) => {