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

[SDESK-6737] feature: Support multilingual text fields on Events & Planning items #1768

Merged
merged 18 commits into from
Mar 12, 2023
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
2 changes: 1 addition & 1 deletion client/actions/autosave.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ const save = (original, updates) => (

return Promise.resolve(item);
}, (error) => {
notify.error(
notify.warning(
getErrorMessage(
error,
gettext('Failed to save autosave item.')
Expand Down
4 changes: 2 additions & 2 deletions client/actions/tests/autosave_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,8 @@ describe('actions.autosave', () => {
.then(done.fail, (error) => {
expect(error).toEqual(errorMessage);

expect(services.notify.error.callCount).toBe(1);
expect(services.notify.error.args[0]).toEqual(['Failed!']);
expect(services.notify.warning.callCount).toBe(1);
expect(services.notify.warning.args[0]).toEqual(['Failed!']);

done();
});
Expand Down
80 changes: 73 additions & 7 deletions client/api/contentProfiles.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import {IPlanningContentProfile, IPlanningAPI} from '../interfaces';
import {appConfig} from 'appConfig';
import {IVocabularyItem} from 'superdesk-api';
import {
IPlanningContentProfile,
IPlanningAPI,
IEventOrPlanningItem,
IProfileMultilingualDetails,
IProfileSchemaTypeString,
} from '../interfaces';
import {planningApi, superdeskApi} from '../superdeskApi';

import {profiles} from '../selectors/forms';
Expand All @@ -8,10 +16,11 @@ import {sortProfileGroups} from '../utils/contentProfiles';

import {showModalConnectedToStore} from '../utils/ui';
import {ContentProfileModal} from '../components/ContentProfiles/ContentProfileModal';
import {getUsersDefaultLanguage} from '../utils/users';

const RESOURCE = 'planning_types';

function getAll() {
function getAll(): Promise<Array<IPlanningContentProfile>> {
return superdeskApi.dataApi.query<IPlanningContentProfile>(
RESOURCE,
1,
Expand All @@ -26,13 +35,62 @@ function getAll() {
});
}

function getProfile(contentType: string) {
function getProfile(contentType: string): IPlanningContentProfile {
const {getState} = planningApi.redux.store;

return profiles(getState())[contentType];
}

function patch(original: IPlanningContentProfile, updates: IPlanningContentProfile) {
function getLanguageSchema(profile: IPlanningContentProfile): IProfileSchemaTypeString {
return profile?.schema?.language as IProfileSchemaTypeString ?? {
type: 'string',
required: false,
field_type: 'single_line',
languages: [appConfig.default_language],
multilingual: false,
default_language: appConfig.default_language,
};
}

function isMultilingualEnabled(profile: IPlanningContentProfile): boolean {
return getLanguageSchema(profile).multilingual === true;
}

function getProfileLanguages(profile: IPlanningContentProfile): Array<IVocabularyItem['qcode']> {
return getLanguageSchema(profile).languages || [];
}

function getProfileDefaultLanguage(profile: IPlanningContentProfile): IVocabularyItem['qcode'] {
return getLanguageSchema(profile).default_language || getUsersDefaultLanguage(true) || appConfig.default_language;
}

function getMultilingualFields(profile: IPlanningContentProfile): Array<keyof IEventOrPlanningItem> {
const languageSchema = getLanguageSchema(profile);

if (languageSchema.multilingual !== true) {
return [];
}

return (Object.keys(profile.schema) as Array<keyof IEventOrPlanningItem>)
.filter((fieldName) => {
const field = profile.schema[fieldName];

return fieldName !== 'language' && field?.type === 'string' && field?.multilingual === true;
});
}

function getMultilingualConfig(contentType: string): IProfileMultilingualDetails {
const profile = getProfile(contentType);

return {
isEnabled: isMultilingualEnabled(profile),
defaultLanguage: getProfileDefaultLanguage(profile),
languages: getProfileLanguages(profile),
fields: getMultilingualFields(profile),
};
}

function patch(original: IPlanningContentProfile, updates: IPlanningContentProfile): Promise<IPlanningContentProfile> {
if (original._id == null) {
delete updates._created;
delete updates._updated;
Expand All @@ -45,7 +103,7 @@ function patch(original: IPlanningContentProfile, updates: IPlanningContentProfi
}
}

function showManagePlanningProfileModal() {
function showManagePlanningProfileModal(): Promise<void> {
const {gettext} = superdeskApi.localization;

return showModalConnectedToStore(
Expand Down Expand Up @@ -94,7 +152,7 @@ function showManagePlanningProfileModal() {
);
}

function showManageEventProfileModal() {
function showManageEventProfileModal(): Promise<void> {
const {gettext} = superdeskApi.localization;

return showModalConnectedToStore(
Expand All @@ -119,7 +177,7 @@ function showManageEventProfileModal() {
);
}

function updateProfilesInStore() {
function updateProfilesInStore(): Promise<void> {
const {dispatch} = planningApi.redux.store;

return getAll().then((profileArray) => {
Expand All @@ -141,4 +199,12 @@ export const contentProfiles: IPlanningAPI['contentProfiles'] = {
showManagePlanningProfileModal: showManagePlanningProfileModal,
showManageEventProfileModal: showManageEventProfileModal,
updateProfilesInStore: updateProfilesInStore,
multilingual: {
getLanguageSchema: getLanguageSchema,
isEnabled: isMultilingualEnabled,
getLanguages: getProfileLanguages,
getFields: getMultilingualFields,
getConfig: getMultilingualConfig,
},
getDefaultLanguage: getProfileDefaultLanguage,
};
49 changes: 49 additions & 0 deletions client/api/editor/events.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {createRef} from 'react';

import {IVocabularyItem} from 'superdesk-api';
import {
EDITOR_TYPE,
IEditorAPI,
Expand Down Expand Up @@ -163,6 +164,53 @@ export function getEventsInstance(type: EDITOR_TYPE): IEditorAPI['events'] {
}
}

function beforeFormUpdates(newState: Partial<IEditorState>, field: keyof IEventOrPlanningItem, value?: any) {
const editorApi = planningApi.editor(type);
const itemType = editorApi.item.getItemType();
const multilingualConfig = planningApi.contentProfiles.multilingual.getConfig(itemType);
const currentState = editorApi.form.getState();

if (multilingualConfig.isEnabled) {
const newDiff = newState.diff;
const languages: Array<IVocabularyItem['qcode']> = (
field === 'languages' ? value : newDiff.languages
) || [];

if (field === 'translations') {
// Make sure the parent field of the translated ones are populated
const translationFields = value.reduce((items, item) => {
items[item.field] = items[item.field] || {};
items[item.field][item.language] = item.value;

return items;
}, {});

multilingualConfig.fields.forEach((parentField) => {
if (translationFields[parentField] != null) {
newDiff[parentField] = (
translationFields[parentField][multilingualConfig.defaultLanguage] ||
translationFields[parentField][languages[0]]
);
}
});
} else if (field === 'languages') {
// Make sure that the parent language field is populated as well
newDiff.language = value[0];

// And filter out any translations that are not in item's selected languages
newDiff.translations = (newDiff.translations || []).filter(
(item) => (languages.includes(item.language))
);

if (currentState.mainLanguage != null && !languages.includes(currentState.mainLanguage)) {
// List of languages has changed, and the `mainLanguage` is no longer available
// So unset it now
newState.mainLanguage = undefined;
}
}
}
}

return {
onEditorConstructed,
onEditorMounted,
Expand All @@ -175,5 +223,6 @@ export function getEventsInstance(type: EDITOR_TYPE): IEditorAPI['events'] {
onOriginalChanged,
onItemUpdated,
onScroll,
beforeFormUpdates,
};
}
19 changes: 19 additions & 0 deletions client/api/editor/form.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {IVocabularyItem} from 'superdesk-api';
import {
EDITOR_TYPE,
IEditorAPI,
Expand All @@ -8,6 +9,7 @@ import {
import {planningApi} from '../../superdeskApi';

import {isItemReadOnly} from '../../utils';
import {getUserInterfaceLanguageFromCV} from '../../utils/users';
import {editorSelectors} from '../../selectors/editors';
import * as actions from '../../actions';

Expand Down Expand Up @@ -139,6 +141,20 @@ export function getFormInstance(type: EDITOR_TYPE): IEditorAPI['form'] {
dispatch(actions.editors.hidePopupForm(type));
}

function getMainLanguage(): IVocabularyItem['qcode'] {
const state = getState();

return state.mainLanguage ?? state.diff.language ?? getUserInterfaceLanguageFromCV();
}

function setMainLanguage(languageQcode?: IVocabularyItem['qcode']) {
setState({mainLanguage: languageQcode});
}

function toggleAllLanguages(): void {
setState({showAllLanguages: !getState().showAllLanguages});
}

return {
setState,
getState,
Expand All @@ -152,5 +168,8 @@ export function getFormInstance(type: EDITOR_TYPE): IEditorAPI['form'] {
waitForScroll,
showPopupForm,
closePopupForm,
getMainLanguage,
setMainLanguage,
toggleAllLanguages,
};
}
21 changes: 20 additions & 1 deletion client/components/ContentProfiles/ContentProfileModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
IEditorProfileGroup,
IProfileFieldEntry,
IG2ContentType,
IProfileSchemaTypeString,
} from '../../interfaces';
import {superdeskApi, planningApi} from '../../superdeskApi';

Expand Down Expand Up @@ -349,7 +350,25 @@ class ContentProfileModalComponent extends React.Component<IProps, IState> {

_updateField<T extends IProfileStateKey>(key: T, item: IProfileFieldEntry) {
this.setState<T>((prevState: Readonly<IState>) => {
const profile = {...prevState[key]};
const profile = cloneDeep(prevState[key]);

if (key === 'profile' && item.schema.type === 'string' && item.name === 'language') {
const enabledBefore = (prevState[key].schema.language as IProfileSchemaTypeString).multilingual;
const enabledAfter = item.schema.multilingual;

if (enabledBefore !== enabledAfter && enabledAfter === false) {
item.schema.languages = null;
item.schema.default_language = null;

Object.keys(profile.schema).forEach((field) => {
const schema = profile.schema[field];

if (schema?.type === 'string') {
schema.multilingual = false;
}
});
}
}

profile.editor[item.name] = {...item.field};
profile.schema[item.name] = {...item.schema};
Expand Down
Loading