Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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,94 @@
/*
* 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';

const TextWidgetMultiLang = (props) => {

const { formData, onChange, schema, required, formContext } = props;
const id = props.id || Math.random().toString(36).substring(7);
const { title, description } = schema;

// TODO map langs from schema when iso lang is change to 2 chars https://github.com/GeoNode/geonode/issues/13643#issuecomment-3728578749
const languages = ["en", "hy", "ru"];
const languageLong = (langCode) => {
return {
en: "English",
hy: "Armenian",
ru: "Russian"
}[langCode] || langCode.toUpperCase();
};

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);
};

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={`Type textarea in ${languageLong(currentLang)}...`}
/>
) :
<input
type="text"
className="form-control"
value={values[currentLang] || ""}
onChange={handleInputChange}
onBlur={() => {}}
placeholder={`Type text in ${languageLong(currentLang)}...`}
/>
}

<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)}
>
{languageLong(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,68 @@ function MetadataEditor({
};
}, []);

const formTitle = metadata?.title || getMessageById(messages, 'gnviewer.metadataEditorTitle', 'Metadata Editor');

/**
* 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'] === true) {
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'] === true && 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 +163,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'] === true) {
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 +234,14 @@ function MetadataEditor({
readonly={readOnly}
ref={initialize.current}
formContext={{
title: metadata?.title,
metadata,
title: formTitle,
metadata: metadataMultiLang,
capitalizeTitle: capitalizeFieldTitle
}}
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 +286,5 @@ MetadataEditor.defaultProps = {
};

export default MetadataEditor;


Loading