Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion geonode_mapstore_client/client/js/actions/gnresource.js
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ export function setResourceExtent(coords) {

export function updateResourceExtent() {
return {
type: UPDATE_RESOURCE_EXTENT,
type: UPDATE_RESOURCE_EXTENT
};
}

Expand Down
2 changes: 1 addition & 1 deletion geonode_mapstore_client/client/js/api/geonode/v2/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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,
Expand All @@ -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';
Expand Down Expand Up @@ -179,6 +183,11 @@ const SchemaField = (props) => {
return <Autocomplete {...autoCompleteProps}/>;
}

// this override ObjectFieldTemplate
if (uiWidget === 'textwidgetmultilang') {
return <TextWidgetMultiLang {...props} />;
}

const hideLabel = shouldHideLabel(props);
return (
<DefaultSchemaField
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ RootMetadata.contextTypes = {
};

function ObjectFieldTemplate(props) {

const isRoot = props?.idSchema?.$id === 'root';
if (isRoot) {
return <RootMetadata {...props} />;
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<div className="form-group field field-string multilang-widget">
<label className={`control-label${formContext?.capitalizeTitle ? ' capitalize' : ''}`}>
{title}
{required && <span className="required">{' '}*</span>}
{!isEmpty(description) ? <>{' '}
<IconWithTooltip tooltip={description} tooltipPosition={"right"} />
</> : null}
</label>
{isTextarea ? (
<DefaultTextareaWidget
id={id}
value={values[currentLang] || ""}
onChange={handleInputChange}
onFocus={() => {}}
onBlur={() => {}}
options={{ rows: 5 }}
placeholder={placeholder}
/>
) :
<input
id={id}
type="text"
className="form-control"
value={values[currentLang] || ""}
onChange={handleInputChange}
onBlur={() => {}}
placeholder={placeholder}
/>
}

<div className="multilang-widget-buttons">
{languages.map((lang) => (
<button
key={lang}
type="button"
className={`btn btn-xs ${currentLang === lang ? "btn-primary" : "btn-outline-secondary"}`}
onClick={() => setCurrentLang(lang)}
>
{getLanguageName(lang)}
</button>
))}
</div>
</div>
);
};


export default TextWidgetMultiLang;
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@

import SelectWidget from './SelectWidget';
import TextareaWidget from './TextareaWidget';
import TextWidgetMultiLang from './TextWidgetMultiLang';

export default {
SelectWidget,
TextareaWidget
TextareaWidget,
TextWidgetMultiLang
};
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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';

Expand Down Expand Up @@ -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) {
Expand All @@ -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 (
<div className="gn-metadata">
<div className="gn-metadata-header">
Expand All @@ -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}
Expand Down Expand Up @@ -169,3 +285,5 @@ MetadataEditor.defaultProps = {
};

export default MetadataEditor;


Loading