Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[Enhancement #393] Support multilingual label and comment in new property form #488

Merged
merged 3 commits into from
Jul 25, 2024
Merged
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
7 changes: 6 additions & 1 deletion src/action/AsyncActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import { ConsolidatedResults } from "../model/ConsolidatedResults";
import UserRole, { UserRoleData } from "../model/UserRole";
import { loadTermCount } from "./AsyncVocabularyActions";
import { getApiPrefix } from "./ActionUtils";
import { getShortLocale } from "../util/IntlUtil";

/*
* Asynchronous actions involve requests to the backend server REST API. As per recommendations in the Redux docs, this consists
Expand Down Expand Up @@ -1039,10 +1040,14 @@ export function getLabel(iri: string) {
if (pendingGetLabelRequests[iri] !== undefined) {
return pendingGetLabelRequests[iri];
}

// currently active language
const locale = getShortLocale(getState().intl.locale);

dispatch(asyncActionRequest(action, true));
const promise = Ajax.get(
Constants.API_PREFIX + "/data/label",
param("iri", iri)
param("iri", iri).param("language", locale)
)
.then((data) => {
const payload = {};
Expand Down
96 changes: 82 additions & 14 deletions src/component/genericmetadata/CreatePropertyForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ import CustomInput from "../misc/CustomInput";
import VocabularyUtils from "../../util/VocabularyUtils";
import { useI18n } from "../hook/useI18n";
import TextArea from "../misc/TextArea";
import { langString } from "../../model/MultilingualString";
import { useSelector } from "react-redux";
import TermItState from "../../model/TermItState";
import MultilingualString from "../../model/MultilingualString";
import EditLanguageSelector from "../multilingual/EditLanguageSelector";
import MultilingualIcon from "../misc/MultilingualIcon";

interface CreatePropertyFormProps {
onOptionCreate: (option: RdfsResourceData) => void;
toggleModal: () => void;
languages: string[];
language: string;
}

function isValid(data: { iri: string }) {
Expand All @@ -31,54 +33,120 @@ function isValid(data: { iri: string }) {
const CreatePropertyForm: React.FC<CreatePropertyFormProps> = ({
onOptionCreate,
toggleModal,
languages,
language,
}) => {
const { i18n } = useI18n();
const [iri, setIri] = useState("");
const [label, setLabel] = useState("");
const [comment, setComment] = useState("");
const language = useSelector(
(state: TermItState) => state.configuration.language
);
const [label, setLabel] = useState({} as MultilingualString);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer generics (useState<MultilingualString>({})) over casting, but that's a minor detail.

const [comment, setComment] = useState({} as MultilingualString);
const [modalLanguages, setModalLanguages] = useState([...languages]);
const [modalLanguage, setModalLanguage] = useState(language);

const optimizeOrUndefined = (string: MultilingualString) => {
const optimized = Object.assign({}, string);
Object.keys(string).forEach((lang) => {
if (string[lang] && string[lang].trim() === "") {
delete optimized[lang];
}
});
if (Object.keys(optimized).length === 0) {
return undefined;
}
return optimized;
};

const onCreate = () => {
toggleModal();

const newProperty: RdfsResourceData = {
iri,
label: label!.length > 0 ? langString(label, language) : undefined,
comment: comment!.length > 0 ? langString(comment, language) : undefined,
label: optimizeOrUndefined(label),
comment: optimizeOrUndefined(comment),
types: [VocabularyUtils.RDF_PROPERTY],
};
onOptionCreate(newProperty);
};

const setMultilingualString = (
str: MultilingualString,
setter: (value: MultilingualString) => void,
value: string
) => {
const copy = Object.assign({}, str);
copy[modalLanguage] = value;
setter(copy);
};

const removeTranslation = (lang: string) => {
const newLabel = Object.assign({}, label);
delete newLabel[lang];
setLabel(newLabel);

const newComment = Object.assign({}, comment);
delete newComment[language];
setComment(newComment);

const newModalLanguages = modalLanguages.filter((l) => l !== lang);
setModalLanguages(newModalLanguages);
};

const withMultilingualIcon = (text: string) => (
<>
{text}
<MultilingualIcon id={"string-list-edit-multilingual" + Date.now()} />
</>
);

return (
<Modal isOpen={true}>
<ModalHeader toggle={toggleModal}>
{i18n("properties.edit.new")}
</ModalHeader>
<ModalBody>
<Row>
<EditLanguageSelector
key="vocabulary-edit-language-selector"
language={modalLanguage}
existingLanguages={modalLanguages}
onSelect={setModalLanguage}
onRemove={removeTranslation}
/>
</Row>
<Row>
<Col xs={12}>
<CustomInput
name="iri"
label={i18n("properties.edit.new.iri")}
value={iri}
onChange={(e) => setIri(e.currentTarget.value)}
hint={i18n("required")}
/>
</Col>
<Col xs={12}>
<CustomInput
name="label"
label={i18n("properties.edit.new.label")}
onChange={(e) => setLabel(e.currentTarget.value)}
label={withMultilingualIcon(i18n("properties.edit.new.label"))}
value={label[modalLanguage] || ""}
onChange={(e) =>
setMultilingualString(label, setLabel, e.currentTarget.value)
}
hint={i18n("required")}
/>
</Col>
<Col xs={12}>
<TextArea
rows={3}
name="comment"
label={i18n("properties.edit.new.comment")}
onChange={(e) => setComment(e.currentTarget.value)}
label={withMultilingualIcon(i18n("properties.edit.new.comment"))}
value={comment[modalLanguage] || ""}
onChange={(e) =>
setMultilingualString(
comment,
setComment,
e.currentTarget.value
)
}
/>
</Col>
</Row>
Expand Down
4 changes: 4 additions & 0 deletions src/component/genericmetadata/UnmappedPropertiesEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ interface UnmappedPropertiesEditProps extends HasI18n {
knownProperties: RdfsResource[];
createProperty: (property: RdfsResource) => Promise<any>;
clearProperties: () => void;
languages: string[];
language: string;
}

interface UnmappedPropertiesEditState {
Expand Down Expand Up @@ -198,6 +200,8 @@ export class UnmappedPropertiesEdit extends React.Component<
<CreatePropertyForm
onOptionCreate={this.onCreateProperty}
toggleModal={() => this.setState({ showCreatePropertyForm: false })}
languages={this.props.languages}
language={this.props.language}
/>
)}
<FormGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { mockUseI18n } from "../../../__tests__/environment/IntlUtil";
import Generator from "../../../__tests__/environment/Generator";
import VocabularyUtils from "../../../util/VocabularyUtils";
import { mountWithIntl } from "../../../__tests__/environment/Environment";
import { changeInputValue } from "../../../__tests__/environment/TestUtil";

describe("CreatePropertyForm", () => {
let onCreate: (data: RdfsResourceData) => void;
Expand All @@ -17,7 +18,12 @@ describe("CreatePropertyForm", () => {
it("adds rdf:Property to types on create", () => {
mockUseI18n();
const wrapper = mountWithIntl(
<CreatePropertyForm onOptionCreate={onCreate} toggleModal={toggleModal} />
<CreatePropertyForm
onOptionCreate={onCreate}
toggleModal={toggleModal}
languages={["en"]}
language={"en"}
/>
);
const iriInput = wrapper.find("input[name='iri']");
(iriInput.getDOMNode() as HTMLInputElement).value = Generator.generateUri();
Expand All @@ -31,4 +37,35 @@ describe("CreatePropertyForm", () => {
expect(data.types).toBeDefined();
expect(data.types!.indexOf(VocabularyUtils.RDF_PROPERTY)).not.toEqual(-1);
});

it("wont send label and comment as empty strings when they are blank", () => {
const languages = ["en", "cs"];
const language = "en";
const wrapper = mountWithIntl(
<CreatePropertyForm
onOptionCreate={onCreate}
toggleModal={toggleModal}
languages={languages}
language={language}
/>
);
const SPACE_CHARACTER = " ";
const iriInput = wrapper.find("input[name='iri']");
const labelInput = wrapper.find("input[name='label']");
const commentInput = wrapper.find("textarea[name='comment']");
const submitButton = wrapper.find("button#create-property-submit");
const iri = Generator.generateUri();

changeInputValue(iriInput, iri);
changeInputValue(labelInput, SPACE_CHARACTER);
changeInputValue(commentInput, SPACE_CHARACTER);

submitButton.simulate("click");
expect(onCreate).toHaveBeenCalledWith({
iri,
label: undefined,
comment: undefined,
types: [VocabularyUtils.RDF_PROPERTY],
});
});
});
21 changes: 15 additions & 6 deletions src/component/misc/AssetLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { connect } from "react-redux";
import { ThunkDispatch } from "../../util/Types";
import { getLabel } from "../../action/AsyncActions";
import Namespaces from "../../util/Namespaces";
import TermItState from "../../model/TermItState";

interface AssetLabelProps {
iri: string;
shrinkFullIri?: boolean;
getLabel: (iri: string) => Promise<string>;
language: string;
}

interface AssetLabelState {
Expand Down Expand Up @@ -42,7 +44,11 @@ export class AssetLabel extends React.Component<
}

public componentDidUpdate(prevProps: AssetLabelProps): void {
if (prevProps.iri !== this.props.iri) {
if (
prevProps.iri !== this.props.iri ||
// When the language changes, the label needs to be updated.
prevProps.language !== this.props.language
) {
this.loadLabel(this.props.iri);
}
}
Expand Down Expand Up @@ -78,8 +84,11 @@ export class AssetLabel extends React.Component<
}
}

export default connect(undefined, (dispatch: ThunkDispatch) => {
return {
getLabel: (iri: string) => dispatch(getLabel(iri)),
};
})(AssetLabel);
export default connect(
(state: TermItState) => ({ language: state.intl.locale }),
(dispatch: ThunkDispatch) => {
return {
getLabel: (iri: string) => dispatch(getLabel(iri)),
};
}
)(AssetLabel);
5 changes: 5 additions & 0 deletions src/component/term/TermMetadataEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import "./TermMetadata.scss";
import TermScopeNoteEdit from "./TermScopeNoteEdit";
import HelpIcon from "../misc/HelpIcon";
import TermStateSelector from "./state/TermStateSelector";
import Vocabulary from "../../model/Vocabulary";

interface TermMetadataEditProps extends HasI18n {
term: Term;
Expand All @@ -59,6 +60,7 @@ interface TermMetadataEditProps extends HasI18n {
language: string;
selectLanguage: (lang: string) => void;
validationResults: ConsolidatedResults;
vocabulary: Vocabulary;
}

interface TermMetadataEditState extends TermData {
Expand Down Expand Up @@ -463,6 +465,8 @@ export class TermMetadataEdit extends React.Component<
properties={this.state.unmappedProperties}
ignoredProperties={TermMetadataEdit.mappedPropertiesToIgnore()}
onChange={this.onPropertiesChange}
language={language}
languages={Vocabulary.getLanguages(this.state)}
/>
</Col>
</Row>
Expand Down Expand Up @@ -506,5 +510,6 @@ export class TermMetadataEdit extends React.Component<
export default connect((state: TermItState) => {
return {
validationResults: state.validationResults[state.vocabulary.iri],
vocabulary: state.vocabulary,
};
})(injectIntl(withI18n(TermMetadataEdit)));
2 changes: 2 additions & 0 deletions src/component/vocabulary/VocabularyEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ export class VocabularyEdit extends React.Component<
properties={this.state.unmappedProperties}
ignoredProperties={VocabularyEdit.mappedPropertiesToIgnore()}
onChange={this.onPropertiesChange}
languages={Vocabulary.getLanguages(this.props.vocabulary)}
language={language}
/>
</Col>
</Row>
Expand Down
4 changes: 4 additions & 0 deletions src/reducer/TermItReducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,10 @@ function labelCache(
if (action.type === ActionType.GET_LABEL && isAsyncSuccess(action)) {
return Object.assign({}, state, action.payload);
}
// When changing the language, discard the cache and let them reload.
if (action.type === ActionType.SWITCH_LANGUAGE) {
return {};
}
return state;
}

Expand Down