diff --git a/geonode_mapstore_client/client/js/actions/gnresource.js b/geonode_mapstore_client/client/js/actions/gnresource.js index da52044747..b0a544dfc0 100644 --- a/geonode_mapstore_client/client/js/actions/gnresource.js +++ b/geonode_mapstore_client/client/js/actions/gnresource.js @@ -332,7 +332,7 @@ export function setResourceExtent(coords) { export function updateResourceExtent() { return { - type: UPDATE_RESOURCE_EXTENT, + type: UPDATE_RESOURCE_EXTENT }; } diff --git a/geonode_mapstore_client/client/js/api/geonode/v2/index.js b/geonode_mapstore_client/client/js/api/geonode/v2/index.js index ef2ba78ddb..4967caaa00 100644 --- a/geonode_mapstore_client/client/js/api/geonode/v2/index.js +++ b/geonode_mapstore_client/client/js/api/geonode/v2/index.js @@ -742,7 +742,7 @@ export const getMetadataDownloadLinkByPk = (pk) => { export const updateResourceExtent = (pk) => { return axios.put(getEndpointUrl(DATASETS, `/${pk}/recalc-bbox`)) .then(({ data }) => data); -} +}; export default { getEndpoints, diff --git a/geonode_mapstore_client/client/js/plugins/MetadataEditor/components/_fields/SchemaField.jsx b/geonode_mapstore_client/client/js/plugins/MetadataEditor/components/_fields/SchemaField.jsx index 607f4590ee..ff1e27bc3b 100644 --- a/geonode_mapstore_client/client/js/plugins/MetadataEditor/components/_fields/SchemaField.jsx +++ b/geonode_mapstore_client/client/js/plugins/MetadataEditor/components/_fields/SchemaField.jsx @@ -15,6 +15,7 @@ import template from 'lodash/template'; import Autocomplete from '../Autocomplete'; import DefaultSchemaField from '@rjsf/core/lib/components/fields/SchemaField'; import useSchemaReference from './useSchemaReference'; +import TextWidgetMultiLang from '../_widgets/TextWidgetMultiLang'; function findProperty(name, properties) { return Object.keys(properties || {}).some((key) => { @@ -42,6 +43,7 @@ function shouldHideLabel({ * - Fallback to the default `SchemaField` from `@rjsf` when no custom rendering is needed. */ const SchemaField = (props) => { + const { onChange, schema, @@ -53,6 +55,8 @@ const SchemaField = (props) => { required, formContext } = props; + + const uiWidget = uiSchema?.['ui:widget']?.toLowerCase(); const uiOptions = uiSchema?.['ui:options']; const autocomplete = uiOptions?.['geonode-ui:autocomplete']; const isSchemaItemString = schema?.items?.type === 'string'; @@ -179,6 +183,11 @@ const SchemaField = (props) => { return ; } + // this override ObjectFieldTemplate + if (uiWidget === 'textwidgetmultilang') { + return ; + } + const hideLabel = shouldHideLabel(props); return ( ; diff --git a/geonode_mapstore_client/client/js/plugins/MetadataEditor/components/_widgets/TextWidgetMultiLang.jsx b/geonode_mapstore_client/client/js/plugins/MetadataEditor/components/_widgets/TextWidgetMultiLang.jsx new file mode 100644 index 0000000000..4ce55be764 --- /dev/null +++ b/geonode_mapstore_client/client/js/plugins/MetadataEditor/components/_widgets/TextWidgetMultiLang.jsx @@ -0,0 +1,102 @@ +/* + * Copyright 2026, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, { useState } from "react"; +import isEmpty from 'lodash/isEmpty'; +import isString from 'lodash/isString'; + +import DefaultTextareaWidget from '@rjsf/core/lib/components/widgets/TextareaWidget'; +import IconWithTooltip from '../IconWithTooltip'; +import { getMessageById, getSupportedLocales } from '@mapstore/framework/utils/LocaleUtils'; + +const TextWidgetMultiLang = (props) => { + + const { formData, onChange, schema, required, formContext } = props; + + const id = props.idSchema?.$id; + const { title, description } = schema; + + const languages = Object.keys(schema?.properties); + const languageLabels = languages.reduce((acc, lang) => { + const label = schema?.properties?.[lang]?.['geonode:multilang-lang-label']; + acc[lang] = label; + return acc; + }, {}); + + const getLanguageName = (langCode) => { + const languagesNames = getSupportedLocales(); + const langName = languageLabels[langCode] || languagesNames[langCode]?.description + return `${langName} (${langCode})` + }; + + const isTextarea = schema?.['ui:options']?.widget === 'textarea'; + + const [currentLang, setCurrentLang] = useState(languages[0]); + const values = formData || {}; + + const handleInputChange = (ev) => { + const value = isString(ev) ? ev : ev?.target?.value; + const newValue = { + ...values, + [currentLang]: value + }; + + onChange(newValue); + }; + + const placeholder = getMessageById(formContext.messages, "gnviewer.typeText").replace("{lang}", getLanguageName(currentLang)); + + return ( +
+ + {isTextarea ? ( + {}} + onBlur={() => {}} + options={{ rows: 5 }} + placeholder={placeholder} + /> + ) : + {}} + placeholder={placeholder} + /> + } + +
+ {languages.map((lang) => ( + + ))} +
+
+ ); +}; + + +export default TextWidgetMultiLang; diff --git a/geonode_mapstore_client/client/js/plugins/MetadataEditor/components/_widgets/index.js b/geonode_mapstore_client/client/js/plugins/MetadataEditor/components/_widgets/index.js index d733d1510e..683c873831 100644 --- a/geonode_mapstore_client/client/js/plugins/MetadataEditor/components/_widgets/index.js +++ b/geonode_mapstore_client/client/js/plugins/MetadataEditor/components/_widgets/index.js @@ -8,8 +8,10 @@ import SelectWidget from './SelectWidget'; import TextareaWidget from './TextareaWidget'; +import TextWidgetMultiLang from './TextWidgetMultiLang'; export default { SelectWidget, - TextareaWidget + TextareaWidget, + TextWidgetMultiLang }; diff --git a/geonode_mapstore_client/client/js/plugins/MetadataEditor/containers/MetadataEditor.jsx b/geonode_mapstore_client/client/js/plugins/MetadataEditor/containers/MetadataEditor.jsx index ca49d010f9..85d6ee1f08 100644 --- a/geonode_mapstore_client/client/js/plugins/MetadataEditor/containers/MetadataEditor.jsx +++ b/geonode_mapstore_client/client/js/plugins/MetadataEditor/containers/MetadataEditor.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2024, GeoSolutions Sas. + * Copyright 2026, GeoSolutions Sas. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -10,6 +10,7 @@ import React, { useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import validator from '@rjsf/validator-ajv8'; import Form from '@rjsf/core'; + import { Alert } from 'react-bootstrap'; import isEmpty from 'lodash/isEmpty'; @@ -86,9 +87,66 @@ function MetadataEditor({ }; }, []); + /** + * tranform metadata to multilang format managed by widget `TextWidgetMultiLang` using `geonode:multilang-group` property + * see also schemaToMultiLang() schema transformation + * { + * 'title': { + * "en": "Title in English", + * ... + * } + */ + function metadataToMultiLang(metadataSingleLang, schemaSingleLang) { + return { + ...metadataSingleLang, + ...Object.keys(schemaSingleLang?.properties || {}).reduce((acc, key) => { + const property = schemaSingleLang.properties[key]; + if (property?.['geonode:multilang']) { + acc[key] = Object.keys(metadataSingleLang || {}).reduce((langAcc, dataKey) => { + const dataProperty = schemaSingleLang.properties[dataKey]; + if (dataProperty?.['geonode:multilang-group'] === key) { + const itemLang = dataProperty['geonode:multilang-lang']; + langAcc[itemLang] = metadataSingleLang[dataKey]; + } + return langAcc; + }, {}); + } + return acc; + }, {}) + }; + } + + /** + * re-tranform multilang metadata to single lang format to post to backend api + */ + function metadataToSingleLang(metadataMultiLang, schemaMultiLang) { + const result = { ...metadataMultiLang }; + + Object.keys(schemaMultiLang?.properties || {}).forEach(key => { + const property = schemaMultiLang.properties[key]; + if (property?.['geonode:multilang'] && metadataMultiLang[key]) { + Object.entries(metadataMultiLang[key] || {}).forEach(([lang, value]) => { + const singleLangKey = Object.keys(schemaMultiLang.properties).find(k => { + const prop = schemaMultiLang.properties[k]; + return prop?.['geonode:multilang-group'] === key && + prop?.['geonode:multilang-lang'] === lang; + }); + if (singleLangKey) { + result[singleLangKey] = value; + } + }); + // set empty the single lang field + result[key] = ''; + } + }); + + return result; + } + function handleChange(formData) { + const singleFormData = metadataToSingleLang(formData, schema); setUpdateError(null); - setMetadata(formData); + setMetadata(singleFormData); } if (loading) { @@ -103,6 +161,63 @@ function MetadataEditor({ return null; } + /** + * tranform schema to multilang schema, by `geonode:multilang-group` property + * { + * 'title': { + * "type": "object", + * "title": "Title multilanguage", + * "description": "same title object for multiple languages", + * "properties": { + * "en": {"type": "string" ...}, + * "hy": {"type": "string" ...}, + * "ru": {"type": "string" ...} + * } + * } + * @param {*} schema + * @param {*} uiSchemaMultiLang + * @returns + */ + function schemaToMultiLang(schemaSingleLang, uiSchemaSingleLang) { + const uiSchemaMultiLang = { ...uiSchemaSingleLang }; + const schemaMultiLang = { + ...schemaSingleLang, + properties: Object.keys(schemaSingleLang?.properties || {}).reduce((acc, key) => { + const property = { ...schemaSingleLang.properties[key] }; + if (property?.['geonode:multilang']) { + const newProperty = { + ...property, + type: 'object', + properties: {}, + 'ui:widget': "TextWidgetMultiLang", + 'ui:options': {} + }; + delete newProperty.maxLength; + acc[key] = newProperty; + // set custom widget for multilang text + uiSchemaMultiLang[key] = { + "ui:widget": "TextWidgetMultiLang" + }; + } else if (property?.['geonode:multilang-group']) { + const groupKey = property['geonode:multilang-group']; + const itemLang = property['geonode:multilang-lang']; + acc[groupKey].properties[itemLang] = property; + acc[groupKey]['ui:options'] = { + ...acc[groupKey]['ui:options'], + widget: property['ui:options']?.widget + }; + } else { + acc[key] = property; + } + return acc; + }, {}) + }; + return { schemaMultiLang, uiSchemaMultiLang }; + } + + const {schemaMultiLang, uiSchemaMultiLang} = schemaToMultiLang(schema, uiSchema); + const metadataMultiLang = metadataToMultiLang(metadata, schema); + return (
@@ -117,14 +232,15 @@ function MetadataEditor({ readonly={readOnly} ref={initialize.current} formContext={{ - title: metadata?.title, - metadata, - capitalizeTitle: capitalizeFieldTitle + title: metadata.title || metadataMultiLang.title.en || getMessageById(messages, 'gnviewer.metadataEditorTitle'), + metadata: metadataMultiLang, + capitalizeTitle: capitalizeFieldTitle, + messages }} - schema={schema} + schema={schemaMultiLang} + uiSchema={uiSchemaMultiLang} + formData={metadataMultiLang} widgets={widgets} - uiSchema={uiSchema} - formData={metadata} validator={validator} templates={templates} fields={fields} @@ -169,3 +285,5 @@ MetadataEditor.defaultProps = { }; export default MetadataEditor; + + diff --git a/geonode_mapstore_client/client/js/plugins/Operation/components/UploadPanel.jsx b/geonode_mapstore_client/client/js/plugins/Operation/components/UploadPanel.jsx index ea8b37d106..e622710f22 100644 --- a/geonode_mapstore_client/client/js/plugins/Operation/components/UploadPanel.jsx +++ b/geonode_mapstore_client/client/js/plugins/Operation/components/UploadPanel.jsx @@ -57,7 +57,7 @@ function UploadPanel({ remoteTypeErrorMessageId, remoteTypeFromUrl, isRemoteTypesDisabled, - uploadActions=[{ labelId: 'gnviewer.upload' }], + uploadActions = [{ labelId: 'gnviewer.upload' }] }) { const inputFile = useRef(); @@ -113,7 +113,6 @@ function UploadPanel({ return handleAdd([getDefaultRemoteResource({ id: uuidv1(), type: 'remote', url: '' })]); }; - const supportedLabels = uniq(supportedFiles.map(supportedFile => supportedFile.label)).join(', '); const uploadsList = uploads.filter(upload => upload.type === 'file' ? upload.supported : true); @@ -224,20 +223,20 @@ function UploadPanel({ : !loading ? ( - <> - {uploadActions?.map(({ labelId, variant, action, showConfirm: shouldConfirm }, id) => ( - - ))} + <> + {uploadActions?.map(({ labelId, variant, action, showConfirm: shouldConfirm }, id) => ( + + ))} - ) :