From e1f48e53983930d81a45980706ccd9a2f8348cb8 Mon Sep 17 00:00:00 2001 From: pmusaraj Date: Fri, 15 Jun 2018 13:01:25 -0400 Subject: [PATCH 01/95] WIP of form translations UI --- jsapp/js/components/formLanding.es6 | 87 ++++---- jsapp/js/components/formLanguages.es6 | 50 +++++ jsapp/js/components/formSubScreens.es6 | 4 +- jsapp/js/components/modal.es6 | 23 +- .../copyTeamPermissions.es6 | 0 .../{ => modalForms}/sharingForm.es6 | 13 +- .../{ => modalForms}/submission.es6 | 23 +- .../{ => modalForms}/tableColumnFilter.es6 | 15 +- .../modalForms/translationSettings.es6 | 205 ++++++++++++++++++ jsapp/js/utils.es6 | 25 +++ jsapp/scss/components/_kobo.form-view.scss | 47 +--- jsapp/scss/components/_kobo.modal.scss | 58 +++++ 12 files changed, 423 insertions(+), 127 deletions(-) create mode 100644 jsapp/js/components/formLanguages.es6 rename jsapp/js/components/{sharingForm => modalForms}/copyTeamPermissions.es6 (100%) rename jsapp/js/components/{ => modalForms}/sharingForm.es6 (98%) rename jsapp/js/components/{ => modalForms}/submission.es6 (98%) rename jsapp/js/components/{ => modalForms}/tableColumnFilter.es6 (97%) create mode 100644 jsapp/js/components/modalForms/translationSettings.es6 diff --git a/jsapp/js/components/formLanding.es6 b/jsapp/js/components/formLanding.es6 index 1e7d8ca2d9..cffd931799 100644 --- a/jsapp/js/components/formLanding.es6 +++ b/jsapp/js/components/formLanding.es6 @@ -29,17 +29,10 @@ export class FormLanding extends React.Component { constructor(props){ super(props); this.state = { - questionLanguageIndex: 0, selectedCollectMethod: 'offline_url' }; autoBind(this); } - componentWillReceiveProps() { - this.setState({ - questionLanguageIndex: 0 - } - ); - } enketoPreviewModal (evt) { evt.preventDefault(); stores.pageState.showModal({ @@ -47,9 +40,7 @@ export class FormLanding extends React.Component { assetid: this.state.uid }); } - renderFormInfo () { - const userCanEdit = this.userCan('change_asset', this.state); - + renderFormInfo (userCanEdit) { var dvcount = this.state.deployed_versions.count; var undeployedVersion = undefined; @@ -103,31 +94,6 @@ export class FormLanding extends React.Component { ); } - // renderFormLanguages () { - // return ( - // - // {t('Languages')} - // {this.state.summary.languages.map((l, i)=>{ - // return ( - // - // {l} - // - // ); - // })} - - // - // ); - // } - updateQuestionListLanguage (evt) { - let i = evt.currentTarget.dataset.index; - this.setState({ - questionLanguageIndex: i - } - ); - } sharingModal (evt) { evt.preventDefault(); stores.pageState.showModal({ @@ -142,6 +108,13 @@ export class FormLanding extends React.Component { asset: this.state }); } + languagesModal (evt) { + evt.preventDefault(); + stores.pageState.showModal({ + type: 'form-languages', + asset: this.state + }); + } renderHistory () { var dvcount = this.state.deployed_versions.count; return ( @@ -339,8 +312,7 @@ export class FormLanding extends React.Component { } ); } - renderButtons () { - const userCanEdit = this.userCan('change_asset', this.state); + renderButtons (userCanEdit) { var downloadable = false; var downloads = []; if (this.state.downloads) { @@ -398,12 +370,43 @@ export class FormLanding extends React.Component { {t('Clone this project')} + + + {t('Add/Edit Languages')} + ); } + renderLanguages () { + if (this.state.content.translations.length < 2) + return false; + + return ( + + + {t('Languages')} + {this.state.content.translations.map((l, i)=>{ + return ( + + {l} + + ); + })} + + + + + + + + ); + } render () { var docTitle = this.state.name || t('Untitled'); + const userCanEdit = this.userCan('change_asset', this.state); if (this.state.uid == undefined) { return ( @@ -429,21 +432,19 @@ export class FormLanding extends React.Component { t('Draft version')} - {this.renderButtons()} + {this.renderButtons(userCanEdit)} - {this.userCan('change_asset', this.state) && this.state.deployed_versions.count > 0 && + {userCanEdit && this.state.deployed_versions.count > 0 && this.state.deployed_version_id != this.state.version_id && this.state.deployment__active && {t('If you want to make these changes public, you must deploy this form.')} } - {this.renderFormInfo()} - {/*this.state.summary && this.state.summary.languages && this.state.summary.languages[0] != null && - this.renderFormLanguages() */ - } + {this.renderFormInfo(userCanEdit)} + {this.renderLanguages(userCanEdit)} {this.state.deployed_versions.count > 0 && diff --git a/jsapp/js/components/formLanguages.es6 b/jsapp/js/components/formLanguages.es6 new file mode 100644 index 0000000000..1cbe68b894 --- /dev/null +++ b/jsapp/js/components/formLanguages.es6 @@ -0,0 +1,50 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import reactMixin from 'react-mixin'; +import autoBind from 'react-autobind'; +import Reflux from 'reflux'; +import actions from '../actions'; +import bem from '../bem'; +import stores from '../stores'; +import ui from '../ui'; +import mixins from '../mixins'; + +import { + formatTime, + t, + notify +} from '../utils'; + +export class FormLanguages extends React.Component { + constructor(props){ + super(props); + autoBind(this); + } + + render() { + return ( + + + {t('Languages')} + {this.props.summary.languages.map((l, i)=>{ + return ( + + {l} + + ); + })} + + + + + + + + ); + } + +}; + +reactMixin(FormLanguages.prototype, mixins.permissions); +export default FormLanguages; diff --git a/jsapp/js/components/formSubScreens.es6 b/jsapp/js/components/formSubScreens.es6 index dc80aba438..6544cdeeaf 100644 --- a/jsapp/js/components/formSubScreens.es6 +++ b/jsapp/js/components/formSubScreens.es6 @@ -4,8 +4,8 @@ import reactMixin from 'react-mixin'; import autoBind from 'react-autobind'; import Reflux from 'reflux'; import _ from 'underscore'; -import {dataInterface} from '../dataInterface'; +import {dataInterface} from '../dataInterface'; import actions from '../actions'; import bem from '../bem'; import stores from '../stores'; @@ -13,7 +13,7 @@ import Select from 'react-select'; import ui from '../ui'; import mixins from '../mixins'; import DocumentTitle from 'react-document-title'; -import SharingForm from '../components/sharingForm'; +import SharingForm from '../components/modalForms/sharingForm'; import DataTable from '../components/table'; import { diff --git a/jsapp/js/components/modal.es6 b/jsapp/js/components/modal.es6 index 2827115f93..c2bb839619 100644 --- a/jsapp/js/components/modal.es6 +++ b/jsapp/js/components/modal.es6 @@ -2,24 +2,22 @@ import React from 'react'; import reactMixin from 'react-mixin'; import autoBind from 'react-autobind'; import Reflux from 'reflux'; +import {hashHistory} from 'react-router'; + import {dataInterface} from '../dataInterface'; import actions from '../actions'; import bem from '../bem'; import ui from '../ui'; import stores from '../stores'; import mixins from '../mixins'; -import {hashHistory} from 'react-router'; -import { - t, - assign, - notify -} from '../utils'; +import {t, notify} from '../utils'; import {ProjectSettings} from '../components/formEditors'; -import SharingForm from '../components/sharingForm'; -import Submission from '../components/submission'; -import TableColumnFilter from '../components/tableColumnFilter'; +import SharingForm from '../components/modalForms/sharingForm'; +import Submission from '../components/modalForms/submission'; +import TableColumnFilter from '../components/modalForms/tableColumnFilter'; +import TranslationSettings from '../components/modalForms/translationSettings'; class Modal extends React.Component { constructor(props) { @@ -83,6 +81,10 @@ class Modal extends React.Component { this.setState({ title: t('Table display options') }); + case 'form-languages': + this.setState({ + title: t('Manage languages and translations') + }); break; } } @@ -223,6 +225,9 @@ class Modal extends React.Component { getColumnLabel={this.props.params.getColumnLabel} overrideLabelsAndGroups={this.props.params.overrideLabelsAndGroups} /> } + { this.props.params.type == 'form-languages' && + + } ) diff --git a/jsapp/js/components/sharingForm/copyTeamPermissions.es6 b/jsapp/js/components/modalForms/copyTeamPermissions.es6 similarity index 100% rename from jsapp/js/components/sharingForm/copyTeamPermissions.es6 rename to jsapp/js/components/modalForms/copyTeamPermissions.es6 diff --git a/jsapp/js/components/sharingForm.es6 b/jsapp/js/components/modalForms/sharingForm.es6 similarity index 98% rename from jsapp/js/components/sharingForm.es6 rename to jsapp/js/components/modalForms/sharingForm.es6 index 38f7ddc7cb..dbf2189e52 100644 --- a/jsapp/js/components/sharingForm.es6 +++ b/jsapp/js/components/modalForms/sharingForm.es6 @@ -5,21 +5,22 @@ import reactMixin from 'react-mixin'; import autoBind from 'react-autobind'; import Reflux from 'reflux'; import TagsInput from 'react-tagsinput'; -import stores from '../stores'; -import actions from '../actions'; -import mixins from '../mixins'; import classNames from 'classnames'; import Select from 'react-select'; -import bem from '../bem'; + +import mixins from 'js/mixins'; +import stores from 'js/stores'; +import actions from 'js/actions'; +import bem from 'js/bem'; import { t, parsePermissions, stringToColor, anonUsername -} from '../utils'; +} from 'utils'; // parts -import CopyTeamPermissions from './sharingForm/copyTeamPermissions'; +import CopyTeamPermissions from './copyTeamPermissions'; var availablePermissions = [ {value: 'view', label: t('View Form')}, diff --git a/jsapp/js/components/submission.es6 b/jsapp/js/components/modalForms/submission.es6 similarity index 98% rename from jsapp/js/components/submission.es6 rename to jsapp/js/components/modalForms/submission.es6 index 16137532b2..ef966801ea 100644 --- a/jsapp/js/components/submission.es6 +++ b/jsapp/js/components/modalForms/submission.es6 @@ -2,20 +2,19 @@ import React from 'react'; import ReactDOM from 'react-dom'; import autoBind from 'react-autobind'; import Reflux from 'reflux'; -import {dataInterface} from '../dataInterface'; -import actions from '../actions'; -import reactMixin from 'react-mixin'; -import mixins from '../mixins'; -import bem from '../bem'; -import {t, notify} from '../utils'; -import stores from '../stores'; -import ui from '../ui'; import alertify from 'alertifyjs'; -import icons from '../../xlform/src/view.icons'; +import reactMixin from 'react-mixin'; import Select from 'react-select'; -import { - VALIDATION_STATUSES -} from '../constants'; + +import {dataInterface} from 'js/dataInterface'; +import actions from 'js/actions'; +import mixins from 'js/mixins'; +import bem from 'js/bem'; +import {t, notify} from 'js/utils'; +import stores from 'js/stores'; +import ui from 'js/ui'; +import icons from '../../../xlform/src/view.icons'; +import {VALIDATION_STATUSES} from 'js/constants'; class Submission extends React.Component { diff --git a/jsapp/js/components/tableColumnFilter.es6 b/jsapp/js/components/modalForms/tableColumnFilter.es6 similarity index 97% rename from jsapp/js/components/tableColumnFilter.es6 rename to jsapp/js/components/modalForms/tableColumnFilter.es6 index 0cb156e781..70804ab2d5 100644 --- a/jsapp/js/components/tableColumnFilter.es6 +++ b/jsapp/js/components/modalForms/tableColumnFilter.es6 @@ -2,18 +2,15 @@ import React from 'react'; import PropTypes from 'prop-types'; import Reflux from 'reflux'; import reactMixin from 'react-mixin'; -import bem from '../bem'; -import ui from '../ui'; -import actions from '../actions'; -import stores from '../stores'; -import mixins from '../mixins'; import Select from 'react-select'; import autoBind from 'react-autobind'; -import { - t, - notify -} from '../utils'; +import bem from 'js/bem'; +import ui from 'js/ui'; +import actions from 'js/actions'; +import stores from 'js/stores'; +import mixins from 'js/mixins'; +import {t, notify} from 'js/utils'; export class TableColumnFilter extends React.Component { constructor(props){ diff --git a/jsapp/js/components/modalForms/translationSettings.es6 b/jsapp/js/components/modalForms/translationSettings.es6 new file mode 100644 index 0000000000..94dd884630 --- /dev/null +++ b/jsapp/js/components/modalForms/translationSettings.es6 @@ -0,0 +1,205 @@ +import $ from 'jquery'; +import React from 'react'; +import PropTypes from 'prop-types'; +import reactMixin from 'react-mixin'; +import autoBind from 'react-autobind'; +import Reflux from 'reflux'; +import alertify from 'alertifyjs'; +import classNames from 'classnames'; + +import bem from 'js/bem'; +import stores from 'js/stores'; +import mixins from 'js/mixins'; +import ui from 'js/ui'; +import actions from 'js/actions'; +import {dataInterface} from 'js/dataInterface'; +import {t,getLangAsObject, getLangString} from 'utils'; + +class LanguageForm extends React.Component { + constructor(props) { + super(props); + this.state = { + languageName: '', + languageCode: '' + } + + if (this.props.langString) { + const lang = getLangAsObject(this.props.langString); + this.state = { + languageName: lang.name || '', + languageCode: lang.code || '' + } + } + autoBind(this); + } + submit() { + if (this.props.langString) { + this.props.addOrUpdateLanguage(this.state, this.props.langIndex); + } else { + this.props.addOrUpdateLanguage(this.state, -1); + } + } + nameChange (e) { + this.setState({languageName: e.target.value}); + } + codeChange (e) { + this.setState({languageCode: e.target.value}); + } + render () { + let fieldsNotEmpty = this.state.languageName.length > 0 && this.state.languageCode.length > 0; + var btnClasses = classNames('mdl-button','mdl-js-button', 'mdl-button--raised', fieldsNotEmpty ? 'mdl-button--colored' : 'mdl-button--disabled'); + + return ( + + + + + + + + + + + + + + + + + + ); + } +}; + +export class TranslationSettings extends React.Component { + constructor(props){ + super(props); + this.state = { + translations: props.asset.content.translations, + showTranslations: false, + showAddLanguageForm: false, + renameLanguageIndex: -1 + } + autoBind(this); + } + showAddLanguageForm() { + this.setState({ + showAddLanguageForm: true + }) + } + hideAddLanguageForm() { + this.setState({ + showAddLanguageForm: false + }) + } + toggleRenameLanguageForm(e) { + let index = parseInt($(e.target).closest('[data-index]').get(0).getAttribute('data-index')); + if (this.state.renameLanguageIndex === index) { + this.setState({ + renameLanguageIndex: -1 + }); + } else { + this.setState({ + renameLanguageIndex: index + }); + } + } + addOrUpdateLanguage(lang, index) { + let content = this.props.asset.content; + if (index > -1) { + content.translations[index] = getLangString(lang); + } else { + content.translations.push(getLangString(lang)); + console.error('this is not ready yet'); + // TODO: show update translation arrays with this new language + // return false; + } + + this.updateAsset(content); + } + updateAsset (content) { + console.log(this.props.asset.uid); + console.log(content.translations); + actions.resources.updateAsset( + this.props.asset.uid, + {content: JSON.stringify(content)} + ); + } + render () { + return ( + + {!this.state.showTranslations && + + + {t('Current languages')} + + {this.state.translations.map((l, i)=> { + return ( + + + + {l} + + + + + + + + + + + + + + {this.state.renameLanguageIndex === i && + + + + } + + ); + })} + {!this.state.showAddLanguageForm && + + + + } + {this.state.showAddLanguageForm && + + + + + + {t('Add a new language')} + + + + } + + } + {this.state.showTranslations && +
+ {t('asdasd 123 - translations for ?')} +
+ } +
+ ); + } +}; + +reactMixin(TranslationSettings.prototype, Reflux.ListenerMixin); + +export default TranslationSettings; diff --git a/jsapp/js/utils.es6 b/jsapp/js/utils.es6 index e2d0208466..65492ad0d1 100644 --- a/jsapp/js/utils.es6 +++ b/jsapp/js/utils.es6 @@ -127,6 +127,31 @@ export function currentLang() { return cookie.load(LANGUAGE_COOKIE_NAME) || 'en'; } +export function getLangAsObject(langString) { + var lang = { + code: '', + name: '' + }; + let opening = langString.indexOf('('); + let closing = langString.indexOf(')'); + let langCode = langString.substring(opening + 1, closing); + + if (langCode) { + lang.code = langCode; + lang.name = langString.substring(0, opening).trim(); + } else { + lang.name = langString; + } + + return lang; +} + +export function getLangString(obj) { + if (obj.languageName && obj.languageCode) { + return `${obj.languageName} (${obj.languageCode})`; + } +} + log.t = function () { let _t = {}; __strings.forEach(function(str){ _t[str] = str; }) diff --git a/jsapp/scss/components/_kobo.form-view.scss b/jsapp/scss/components/_kobo.form-view.scss index bad2edbb0d..5641a48d47 100644 --- a/jsapp/scss/components/_kobo.form-view.scss +++ b/jsapp/scss/components/_kobo.form-view.scss @@ -240,15 +240,8 @@ } &--langButton { - margin-left: 15px; + margin-left: 10px; display: inline-block; - padding: 6px; - border-radius: 6px; - cursor: pointer; - } - - &--langButton.active { - background: #E6E6E8; } &--thin-label { @@ -259,44 +252,6 @@ margin-bottom: 10px; } - &--questions { - align-items: flex-start; - - .question-count { - font-size: 36px; - padding-top: 15px; - } - } - - &--question-list { - margin-left: 40px; - max-height: 260px; - padding: 10px 0px; - overflow: auto; - flex-grow: 2; - - > div { - margin-bottom: 15px; - font-size: 14px; - } - - > div > i { - margin-right: 25px; - opacity: 0.3; - display: inline-block; - vertical-align: middle; - } - - > div > span { - vertical-align: middle; - display: inline-block; - width: calc(100% - 60px); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - } - &--centered { text-align: center; diff --git a/jsapp/scss/components/_kobo.modal.scss b/jsapp/scss/components/_kobo.modal.scss index 9c5154e3f1..2dd1598176 100644 --- a/jsapp/scss/components/_kobo.modal.scss +++ b/jsapp/scss/components/_kobo.modal.scss @@ -468,6 +468,64 @@ } } +.form-modal--translation-settings { + .form-view__cell--translation { + display: flex; + justify-content: space-between; + border-top: 1px solid $divider-color; + padding: 6px 0px; + } + + .form-view__cell--add-language { + margin-top: 20px; + text-align: right; + } + + .form-view__cell--add-language-form, + .form-view__cell--update-language-form { + background: #F8F8F8; + padding: 15px; + position: relative; + + .form-view__link--close { + position: absolute; + right: 0px; + top: 6px; + + i { + font-size: 24px; + } + } + } + + .form-view__cell--add-language-form { + margin-top: 20px; + } + + .form-view__cell--add-language-fields { + display: flex; + justify-content: space-between; + + .form-view__cell { + min-width: 100px; + + &--lang-code { + max-width: 50px; + } + + input { + padding-left: 5px; + max-width: 95%; + } + + button { + margin-left: 10px; + margin-top: 5px; + } + } + } +} + // alertify overrides .alertify .ajs-dimmer { From 803c9262069608d4efa3a981f45f59305ad3d1e7 Mon Sep 17 00:00:00 2001 From: pmusaraj Date: Mon, 18 Jun 2018 10:46:32 -0400 Subject: [PATCH 02/95] fix broken formLanding in forms without translations --- jsapp/js/components/formLanding.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsapp/js/components/formLanding.es6 b/jsapp/js/components/formLanding.es6 index cffd931799..50c4b83f65 100644 --- a/jsapp/js/components/formLanding.es6 +++ b/jsapp/js/components/formLanding.es6 @@ -379,7 +379,7 @@ export class FormLanding extends React.Component { ); } renderLanguages () { - if (this.state.content.translations.length < 2) + if (!this.state.content.translations || this.state.content.translations.length < 2) return false; return ( From e9bb4a748a828bbe8e964938278a09b3973f7867 Mon Sep 17 00:00:00 2001 From: pmusaraj Date: Mon, 18 Jun 2018 15:59:37 -0400 Subject: [PATCH 03/95] more WIP on adding languages/translations to projets via UI --- jsapp/js/components/formLanding.es6 | 10 +- .../modalForms/translationSettings.es6 | 142 +++++++++++++++--- jsapp/scss/components/_kobo.modal.scss | 1 + 3 files changed, 129 insertions(+), 24 deletions(-) diff --git a/jsapp/js/components/formLanding.es6 b/jsapp/js/components/formLanding.es6 index 50c4b83f65..8e4c892a9c 100644 --- a/jsapp/js/components/formLanding.es6 +++ b/jsapp/js/components/formLanding.es6 @@ -370,10 +370,12 @@ export class FormLanding extends React.Component { {t('Clone this project')} - - - {t('Add/Edit Languages')} - + {this.state.content.survey.length > 0 && + + + {t('Add/Edit Languages')} + + } ); diff --git a/jsapp/js/components/modalForms/translationSettings.es6 b/jsapp/js/components/modalForms/translationSettings.es6 index 94dd884630..f65e64eeba 100644 --- a/jsapp/js/components/modalForms/translationSettings.es6 +++ b/jsapp/js/components/modalForms/translationSettings.es6 @@ -13,7 +13,7 @@ import mixins from 'js/mixins'; import ui from 'js/ui'; import actions from 'js/actions'; import {dataInterface} from 'js/dataInterface'; -import {t,getLangAsObject, getLangString} from 'utils'; +import {t,getLangAsObject, getLangString, notify} from 'utils'; class LanguageForm extends React.Component { constructor(props) { @@ -33,7 +33,7 @@ class LanguageForm extends React.Component { autoBind(this); } submit() { - if (this.props.langString) { + if (this.props.langIndex !== undefined) { this.props.addOrUpdateLanguage(this.state, this.props.langIndex); } else { this.props.addOrUpdateLanguage(this.state, -1); @@ -65,7 +65,7 @@ class LanguageForm extends React.Component { @@ -77,13 +77,28 @@ export class TranslationSettings extends React.Component { constructor(props){ super(props); this.state = { - translations: props.asset.content.translations, + translations: props.asset.content.translations || [], showTranslations: false, showAddLanguageForm: false, renameLanguageIndex: -1 } autoBind(this); } + componentDidMount () { + this.listenTo(stores.asset, this.assetStoreChange); + } + assetStoreChange(asset) { + let uid = this.props.asset.uid; + + this.setState({ + translations: asset[uid].content.translations + }) + + stores.pageState.showModal({ + type: 'form-languages', + asset: asset[uid] + }); + } showAddLanguageForm() { this.setState({ showAddLanguageForm: true @@ -112,22 +127,103 @@ export class TranslationSettings extends React.Component { content.translations[index] = getLangString(lang); } else { content.translations.push(getLangString(lang)); - console.error('this is not ready yet'); - // TODO: show update translation arrays with this new language - // return false; + content = this.prepareTranslations(content); } this.updateAsset(content); } + deleteLanguage(e) { + let index = parseInt($(e.target).closest('[data-index]').get(0).getAttribute('data-index')); + let content = this.props.asset.content; + content = this.deleteTranslations(content, index); + if (content) { + content.translations.splice(index, 1); + let dialog = alertify.dialog('confirm'); + let opts = { + title: t('Delete language?'), + message: t('Are you sure you want to delete this language? This action is not reversible.'), + labels: {ok: t('Delete'), cancel: t('Cancel')}, + onok: () => { + this.updateAsset(content); + dialog.destroy(); + }, + oncancel: () => {dialog.destroy()} + }; + dialog.set(opts).show(); + + } else { + notify('Error: translation index mismatch. Cannot delete language.'); + } + + } + prepareTranslations(content) { + // prepare all translation arrays when adding a language + let translated = content.translated, + translationsLength = content.translations.length, + survey = content.survey, + choices = content.choices; + + // append null values to translations for each survey row + for (var i = 0, len = survey.length; i < len; i++) { + let row = survey[i]; + for (var j = 0, len2 = translated.length; j < len2; j++) { + var property = translated[j]; + if (row[property] && row[property].length < translationsLength) { + row[property].push(null); + } + } + } + + // append null values to translations for each survey row + for (var i = 0, len = choices.length; i < len; i++) { + if (choices[i].label.length < translationsLength) { + choices[i].label.push(null); + } + } + return content; + } + deleteTranslations(content, langIndex) { + // delete items from translation arrays for this langIndex + let translated = content.translated, + translationsLength = content.translations.length, + survey = content.survey, + choices = content.choices; + + for (var i = 0, len = survey.length; i < len; i++) { + let row = survey[i]; + for (var j = 0, len2 = translated.length; j < len2; j++) { + var property = translated[j]; + if (row[property]) { + if (row[property].length === translationsLength) { + row[property].splice(langIndex, 1); + } else { + console.error('Translations index mismatch'); + return false; + } + } + } + } + + for (var i = 0, len = choices.length; i < len; i++) { + if (choices[i].label) { + if (choices[i].label.length === translationsLength) { + choices[i].label.splice(langIndex, 1); + } else { + console.error('Translations index mismatch'); + return false; + } + } + } + return content; + } updateAsset (content) { - console.log(this.props.asset.uid); - console.log(content.translations); actions.resources.updateAsset( this.props.asset.uid, {content: JSON.stringify(content)} ); } render () { + this.prepareTranslations(this.props.asset.content); return ( {!this.state.showTranslations && @@ -140,7 +236,8 @@ export class TranslationSettings extends React.Component { - {l} + {l ? l : t('Unnamed language')} + {i === 0 ? ` ${t('default')}` : ''} - - - - - - + {i > 0 && + + + + } + {i > 0 && + + + + } {this.state.renameLanguageIndex === i && diff --git a/jsapp/scss/components/_kobo.modal.scss b/jsapp/scss/components/_kobo.modal.scss index 2dd1598176..57b0cee505 100644 --- a/jsapp/scss/components/_kobo.modal.scss +++ b/jsapp/scss/components/_kobo.modal.scss @@ -472,6 +472,7 @@ .form-view__cell--translation { display: flex; justify-content: space-between; + align-items: center; border-top: 1px solid $divider-color; padding: 6px 0px; } From 6110ab899d62dab500db08490220ed59f94e96bc Mon Sep 17 00:00:00 2001 From: pmusaraj Date: Tue, 19 Jun 2018 16:03:46 -0400 Subject: [PATCH 04/95] more WIP for translations UI, added translation table in a modal --- jsapp/js/components/modal.es6 | 10 +++ .../modalForms/translationSettings.es6 | 18 +++- .../modalForms/translationTable.es6 | 84 +++++++++++++++++++ jsapp/scss/components/_kobo.modal.scss | 10 +++ 4 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 jsapp/js/components/modalForms/translationTable.es6 diff --git a/jsapp/js/components/modal.es6 b/jsapp/js/components/modal.es6 index c2bb839619..f0e571e386 100644 --- a/jsapp/js/components/modal.es6 +++ b/jsapp/js/components/modal.es6 @@ -18,6 +18,7 @@ import SharingForm from '../components/modalForms/sharingForm'; import Submission from '../components/modalForms/submission'; import TableColumnFilter from '../components/modalForms/tableColumnFilter'; import TranslationSettings from '../components/modalForms/translationSettings'; +import TranslationTable from '../components/modalForms/translationTable'; class Modal extends React.Component { constructor(props) { @@ -85,6 +86,12 @@ class Modal extends React.Component { this.setState({ title: t('Manage languages and translations') }); + break; + case 'form-translation-table': + this.setState({ + title: t('Translations table'), + modalClass: 'modal-large' + }); break; } } @@ -228,6 +235,9 @@ class Modal extends React.Component { { this.props.params.type == 'form-languages' && } + { this.props.params.type == 'form-translation-table' && + + } ) diff --git a/jsapp/js/components/modalForms/translationSettings.es6 b/jsapp/js/components/modalForms/translationSettings.es6 index f65e64eeba..8f36336d12 100644 --- a/jsapp/js/components/modalForms/translationSettings.es6 +++ b/jsapp/js/components/modalForms/translationSettings.es6 @@ -12,8 +12,8 @@ import stores from 'js/stores'; import mixins from 'js/mixins'; import ui from 'js/ui'; import actions from 'js/actions'; -import {dataInterface} from 'js/dataInterface'; -import {t,getLangAsObject, getLangString, notify} from 'utils'; + +import {t, getLangAsObject, getLangString, notify} from 'utils'; class LanguageForm extends React.Component { constructor(props) { @@ -121,6 +121,18 @@ export class TranslationSettings extends React.Component { }); } } + launchTranslationTableModal(e) { + let index = parseInt($(e.target).closest('[data-index]').get(0).getAttribute('data-index')), + asset = this.props.asset; + stores.pageState.hideModal(); + window.setTimeout(function(){ + stores.pageState.showModal({ + type: 'form-translation-table', + asset: asset, + langIndex: index + }); + }, 300); + } addOrUpdateLanguage(lang, index) { let content = this.props.asset.content; if (index > -1) { @@ -223,7 +235,6 @@ export class TranslationSettings extends React.Component { ); } render () { - this.prepareTranslations(this.props.asset.content); return ( {!this.state.showTranslations && @@ -249,6 +260,7 @@ export class TranslationSettings extends React.Component { {i > 0 && diff --git a/jsapp/js/components/modalForms/translationTable.es6 b/jsapp/js/components/modalForms/translationTable.es6 new file mode 100644 index 0000000000..fc6c74366e --- /dev/null +++ b/jsapp/js/components/modalForms/translationTable.es6 @@ -0,0 +1,84 @@ +import $ from 'jquery'; +import React from 'react'; +import PropTypes from 'prop-types'; +import reactMixin from 'react-mixin'; +import autoBind from 'react-autobind'; +import Reflux from 'reflux'; +import alertify from 'alertifyjs'; +import ReactTable from 'react-table' + +import bem from 'js/bem'; +import stores from 'js/stores'; +import mixins from 'js/mixins'; +import ui from 'js/ui'; +import actions from 'js/actions'; + +import {t, getLangAsObject, getLangString, notify} from 'utils'; + +export class TranslationTable extends React.Component { + constructor(props){ + super(props); + this.state = { + content: props.asset.content, + tableData: [] + } + + let translated = props.asset.content.translated, + survey = props.asset.content.survey, + choices = props.asset.content.choices; + + for (var i = 0, len = survey.length; i < len; i++) { + let row = survey[i]; + for (var j = 0, len2 = translated.length; j < len2; j++) { + var property = translated[j]; + if (row[property]) { + var tableRow = { + original: row[property][0], + translation: row[property][props.langIndex], + name: row.name || row.$autoname, + property: property + } + + this.state.tableData.push(tableRow); + } + } + } + + autoBind(this); + } + + updateAsset (content) { + actions.resources.updateAsset( + this.props.asset.uid, + {content: JSON.stringify(content)} + ); + } + render () { + return ( + + + + + ); + } +}; + +reactMixin(TranslationTable.prototype, Reflux.ListenerMixin); + +export default TranslationTable; diff --git a/jsapp/scss/components/_kobo.modal.scss b/jsapp/scss/components/_kobo.modal.scss index 57b0cee505..66ea0f8d99 100644 --- a/jsapp/scss/components/_kobo.modal.scss +++ b/jsapp/scss/components/_kobo.modal.scss @@ -527,6 +527,16 @@ } } +.form-modal--translation-table { + height: 100%; + display: flex; + width: 100%; + + .ReactTable { + width: 100%; + } +} + // alertify overrides .alertify .ajs-dimmer { From 392a4aa867a0c84128eec6febc209c10b0ffcfb7 Mon Sep 17 00:00:00 2001 From: pmusaraj Date: Wed, 20 Jun 2018 11:46:07 -0400 Subject: [PATCH 05/95] added translations table UI --- .../modalForms/translationTable.es6 | 109 ++++++++++++------ jsapp/scss/components/_kobo.modal.scss | 68 +++++++++++ 2 files changed, 139 insertions(+), 38 deletions(-) diff --git a/jsapp/js/components/modalForms/translationTable.es6 b/jsapp/js/components/modalForms/translationTable.es6 index fc6c74366e..e9e1d5f5aa 100644 --- a/jsapp/js/components/modalForms/translationTable.es6 +++ b/jsapp/js/components/modalForms/translationTable.es6 @@ -1,25 +1,15 @@ -import $ from 'jquery'; -import React from 'react'; -import PropTypes from 'prop-types'; -import reactMixin from 'react-mixin'; -import autoBind from 'react-autobind'; -import Reflux from 'reflux'; -import alertify from 'alertifyjs'; -import ReactTable from 'react-table' +import React from 'react' +import TextareaAutosize from 'react-autosize-textarea' -import bem from 'js/bem'; -import stores from 'js/stores'; -import mixins from 'js/mixins'; -import ui from 'js/ui'; -import actions from 'js/actions'; +import bem from 'js/bem' +import actions from 'js/actions' -import {t, getLangAsObject, getLangString, notify} from 'utils'; +import {t, getLangAsObject, getLangString, notify} from 'utils' export class TranslationTable extends React.Component { constructor(props){ super(props); this.state = { - content: props.asset.content, tableData: [] } @@ -27,6 +17,7 @@ export class TranslationTable extends React.Component { survey = props.asset.content.survey, choices = props.asset.content.choices; + // add each translatable property for survey items to translation table for (var i = 0, len = survey.length; i < len; i++) { let row = survey[i]; for (var j = 0, len2 = translated.length; j < len2; j++) { @@ -36,7 +27,8 @@ export class TranslationTable extends React.Component { original: row[property][0], translation: row[property][props.langIndex], name: row.name || row.$autoname, - property: property + itemProp: property, + contentProp: 'survey' } this.state.tableData.push(tableRow); @@ -44,41 +36,82 @@ export class TranslationTable extends React.Component { } } - autoBind(this); + // add choice options to translation table + for (var i = 0, len = choices.length; i < len; i++) { + let choice = choices[i]; + var tableRow = { + original: choice.label[0], + translation: choice.label[props.langIndex], + name: choice.name || choice.$autovalue, + itemProp: 'label', + contentProp: 'choices' + } + this.state.tableData.push(tableRow); + } } + onChange(value, index) { + let tD = this.state.tableData; + tD[index].translation = value; + this.setState({ + tableData: tD + }); + } + saveChanges() { + var content = this.props.asset.content; + let rows = this.state.tableData, + langIndex = this.props.langIndex; + for (var i = 0, len = rows.length; i < len; i++) { + let contentProp = rows[i].contentProp; + let item = content[rows[i].contentProp].find(o => o.name === rows[i].name || o.$autoname === rows[i].name || o.$autovalue === rows[i].name); + var itemProp = rows[i].itemProp; + + if (item[itemProp][langIndex] !== rows[i].translation) { + item[itemProp][langIndex] = rows[i].translation; + } + } - updateAsset (content) { actions.resources.updateAsset( this.props.asset.uid, {content: JSON.stringify(content)} ); } render () { + let langIndex = this.props.langIndex, + translationLabel = this.props.asset.content.translations[langIndex]; return ( - - + + + + + + + + + + {this.state.tableData.map((item, i)=>{ + return ( + + + + + ); + })} + +
{t('Original string')}{`${translationLabel} ${t('Translation')}`}
{item.original} + this.onChange(e.target.value, i)} + value={item.translation}/> +
+
+ + +
); } }; -reactMixin(TranslationTable.prototype, Reflux.ListenerMixin); - export default TranslationTable; diff --git a/jsapp/scss/components/_kobo.modal.scss b/jsapp/scss/components/_kobo.modal.scss index 66ea0f8d99..e314187ffe 100644 --- a/jsapp/scss/components/_kobo.modal.scss +++ b/jsapp/scss/components/_kobo.modal.scss @@ -530,10 +530,78 @@ .form-modal--translation-table { height: 100%; display: flex; + flex-direction: column; width: 100%; + margin: 0; + + .form-modal__item--translation-table--container { + height: calc(100% - 80px); + overflow: auto; + border: 1px solid $divider-color; + } + + table { + width: 100%; + border-spacing: 0px; + + tr { + th, td { + border-bottom: 1px solid $divider-color; + vertical-align: top; + text-align: left; + padding: 6px 8px; + } + + th:first-child, td:first-child { + width: 30%; + } + + th:last-child, td:last-child { + width: 70%; + } + + td.translation { + padding: 0px; + padding-top: 4px; + background: lighten($cool-green, 41%); + + &.missing {} + } + } + + textarea { + border: none; + background: transparent; + width: 100%; + padding: 4px 8px; + + &:focus { + background: lighten($cool-green, 36%); + } + } + } .ReactTable { width: 100%; + + .rt-thead > .rt-tr, + .rt-tr-group > .rt-tr { + > .rt-th:first-child, > .rt-td:first-child { + min-width: 30%; + color: lighten($cool-gray, 10%); + } + + > .rt-th:last-child, > .rt-td:last-child { + min-width: 70%; + } + } + } + + .form-modal__item--translation-table--actions { + width: 100%; + text-align: right; + padding-top: 20px; + height: 80px; } } From d4832b4ee61b4c0f04669e416cfd52ae66cf7a71 Mon Sep 17 00:00:00 2001 From: pmusaraj Date: Wed, 20 Jun 2018 13:26:04 -0400 Subject: [PATCH 06/95] minor cleanup and fixes --- jsapp/js/components/formLanding.es6 | 2 +- .../modalForms/translationSettings.es6 | 132 +++++++++--------- 2 files changed, 64 insertions(+), 70 deletions(-) diff --git a/jsapp/js/components/formLanding.es6 b/jsapp/js/components/formLanding.es6 index 8e4c892a9c..85e60688aa 100644 --- a/jsapp/js/components/formLanding.es6 +++ b/jsapp/js/components/formLanding.es6 @@ -370,7 +370,7 @@ export class FormLanding extends React.Component { {t('Clone this project')} - {this.state.content.survey.length > 0 && + {userCanEdit && this.state.content.survey.length > 0 && {t('Add/Edit Languages')} diff --git a/jsapp/js/components/modalForms/translationSettings.es6 b/jsapp/js/components/modalForms/translationSettings.es6 index 8f36336d12..f94737623f 100644 --- a/jsapp/js/components/modalForms/translationSettings.es6 +++ b/jsapp/js/components/modalForms/translationSettings.es6 @@ -78,7 +78,6 @@ export class TranslationSettings extends React.Component { super(props); this.state = { translations: props.asset.content.translations || [], - showTranslations: false, showAddLanguageForm: false, renameLanguageIndex: -1 } @@ -91,7 +90,9 @@ export class TranslationSettings extends React.Component { let uid = this.props.asset.uid; this.setState({ - translations: asset[uid].content.translations + translations: asset[uid].content.translations, + showAddLanguageForm: false, + renameLanguageIndex: -1 }) stores.pageState.showModal({ @@ -237,78 +238,71 @@ export class TranslationSettings extends React.Component { render () { return ( - {!this.state.showTranslations && - - - {t('Current languages')} - - {this.state.translations.map((l, i)=> { - return ( - - - - {l ? l : t('Unnamed language')} - {i === 0 ? ` ${t('default')}` : ''} - - - - - - {i > 0 && - - - - } - {i > 0 && - - - - } - - - {this.state.renameLanguageIndex === i && - - - + + + {t('Current languages')} + + {this.state.translations.map((l, i)=> { + return ( + + + + {l ? l : t('Unnamed language')} + {i === 0 ? ` ${t('default')}` : ''} + + + + + + {i > 0 && + + + + } + {i > 0 && + + + } - - ); - })} - {!this.state.showAddLanguageForm && - - - - } - {this.state.showAddLanguageForm && - - - - - - {t('Add a new language')} - - } - + {this.state.renameLanguageIndex === i && + + + + } + + ); + })} + {!this.state.showAddLanguageForm && + + + } - {this.state.showTranslations && -
- {t('asdasd 123 - translations for ?')} -
+ {this.state.showAddLanguageForm && + + + + + + {t('Add a new language')} + + + } +
); } From 58750d60c476b66287210ad629be8cd5ecc9e39c Mon Sep 17 00:00:00 2001 From: pmusaraj Date: Wed, 20 Jun 2018 13:46:09 -0400 Subject: [PATCH 07/95] fix bug when adding translation of a form without a choice list --- .../modalForms/translationSettings.es6 | 8 ++++--- .../modalForms/translationTable.es6 | 22 ++++++++++--------- jsapp/scss/components/_kobo.modal.scss | 5 +++++ 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/jsapp/js/components/modalForms/translationSettings.es6 b/jsapp/js/components/modalForms/translationSettings.es6 index f94737623f..4694631923 100644 --- a/jsapp/js/components/modalForms/translationSettings.es6 +++ b/jsapp/js/components/modalForms/translationSettings.es6 @@ -188,9 +188,11 @@ export class TranslationSettings extends React.Component { } // append null values to translations for each survey row - for (var i = 0, len = choices.length; i < len; i++) { - if (choices[i].label.length < translationsLength) { - choices[i].label.push(null); + if (content.choices && content.choices.length) { + for (var i = 0, len = choices.length; i < len; i++) { + if (choices[i].label.length < translationsLength) { + choices[i].label.push(null); + } } } return content; diff --git a/jsapp/js/components/modalForms/translationTable.es6 b/jsapp/js/components/modalForms/translationTable.es6 index e9e1d5f5aa..632880632e 100644 --- a/jsapp/js/components/modalForms/translationTable.es6 +++ b/jsapp/js/components/modalForms/translationTable.es6 @@ -37,16 +37,18 @@ export class TranslationTable extends React.Component { } // add choice options to translation table - for (var i = 0, len = choices.length; i < len; i++) { - let choice = choices[i]; - var tableRow = { - original: choice.label[0], - translation: choice.label[props.langIndex], - name: choice.name || choice.$autovalue, - itemProp: 'label', - contentProp: 'choices' + if (choices && choices.length) { + for (var i = 0, len = choices.length; i < len; i++) { + let choice = choices[i]; + var tableRow = { + original: choice.label[0], + translation: choice.label[props.langIndex], + name: choice.name || choice.$autovalue, + itemProp: 'label', + contentProp: 'choices' + } + this.state.tableData.push(tableRow); } - this.state.tableData.push(tableRow); } } onChange(value, index) { @@ -96,7 +98,7 @@ export class TranslationTable extends React.Component { this.onChange(e.target.value, i)} - value={item.translation}/> + value={item.translation || ''}/> ); diff --git a/jsapp/scss/components/_kobo.modal.scss b/jsapp/scss/components/_kobo.modal.scss index e314187ffe..0a3e62c646 100644 --- a/jsapp/scss/components/_kobo.modal.scss +++ b/jsapp/scss/components/_kobo.modal.scss @@ -477,6 +477,11 @@ padding: 6px 0px; } + .form-view__cell--translation-actions { + min-width: 150px; + text-align: right; + } + .form-view__cell--add-language { margin-top: 20px; text-align: right; From 0e05344c6a92584be548010dda53ec60dc2bd77a Mon Sep 17 00:00:00 2001 From: pmusaraj Date: Fri, 27 Jul 2018 15:44:19 -0400 Subject: [PATCH 08/95] minor UI improvements to translations modal --- jsapp/js/components/formLanding.es6 | 30 ++++++++++++------- .../modalForms/translationSettings.es6 | 19 +++++++++--- jsapp/scss/components/_kobo.modal.scss | 6 +++- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/jsapp/js/components/formLanding.es6 b/jsapp/js/components/formLanding.es6 index d4a77d1125..eabcd153b3 100644 --- a/jsapp/js/components/formLanding.es6 +++ b/jsapp/js/components/formLanding.es6 @@ -309,6 +309,7 @@ export class FormLanding extends React.Component { this.setState({selectedCollectMethod: evt.currentTarget.dataset.method}); } renderButtons (userCanEdit) { + let translations = this.state.content.translations; var downloadable = false; var downloads = []; if (this.state.downloads) { @@ -375,22 +376,27 @@ export class FormLanding extends React.Component { {userCanEdit && this.state.content.survey.length > 0 && - {t('Add/Edit Languages')} + {(!translations || translations.length < 2) ? + t('Add Translations') + : + t('Manage Translations') + } } ); } - renderLanguages () { - if (!this.state.content.translations || this.state.content.translations.length < 2) + renderLanguages (canEdit) { + let translations = this.state.content.translations; + if (!translations || translations.length < 2) return false; return ( {t('Languages')} - {this.state.content.translations.map((l, i)=>{ + {translations.map((l, i)=>{ return ( {l} @@ -398,13 +404,15 @@ export class FormLanding extends React.Component { ); })} - - - - - + {canEdit && + + + + + + } ); } diff --git a/jsapp/js/components/modalForms/translationSettings.es6 b/jsapp/js/components/modalForms/translationSettings.es6 index 75d603d5a0..9cfc1bb2aa 100644 --- a/jsapp/js/components/modalForms/translationSettings.es6 +++ b/jsapp/js/components/modalForms/translationSettings.es6 @@ -238,13 +238,24 @@ export class TranslationSettings extends React.Component { ); } render () { + let translations = this.state.translations; return ( - - {t('Current languages')} - - {this.state.translations.map((l, i)=> { + {(translations && translations[0] === null) ? + + {t('Here you can add one more more languages to your project, and translate the strings in each language.')} +   + + {t('Note: make sure your default language has a name. If it doesn\'t, you will not be able to edit your form in the form builder.')} + + + : + + {t('Current languages')} + + } + {translations.map((l, i)=> { return ( diff --git a/jsapp/scss/components/_kobo.modal.scss b/jsapp/scss/components/_kobo.modal.scss index 54bb68eb47..643c2da1f1 100644 --- a/jsapp/scss/components/_kobo.modal.scss +++ b/jsapp/scss/components/_kobo.modal.scss @@ -471,6 +471,9 @@ } .form-modal--translation-settings { + .form-view__cell--translation-note { + padding-bottom: 20px; + } .form-view__cell--translation { display: flex; justify-content: space-between; @@ -485,8 +488,9 @@ } .form-view__cell--add-language { - margin-top: 20px; + padding-top: 20px; text-align: right; + border-top: 1px solid $divider-color; } .form-view__cell--add-language-form, From 0a9a1ded629bd625819fed0f01d48b2d6c20ca87 Mon Sep 17 00:00:00 2001 From: pmusaraj Date: Fri, 27 Jul 2018 16:11:57 -0400 Subject: [PATCH 09/95] add modal constants for translation modals --- jsapp/js/components/modal.es6 | 12 ++++++++++-- jsapp/js/constants.es6 | 4 +++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/jsapp/js/components/modal.es6 b/jsapp/js/components/modal.es6 index 8db55416a3..eff7b03d33 100644 --- a/jsapp/js/components/modal.es6 +++ b/jsapp/js/components/modal.es6 @@ -85,6 +85,14 @@ class Modal extends React.Component { this.setModalTitle(t('Table display options')); break; + case MODAL_TYPES.FORM_LANGUAGES: + this.setModalTitle(t('Manage languages')); + break; + + case MODAL_TYPES.FORM_TRANSLATIONS_TABLE: + this.setModalTitle(t('Translations table')); + break; + default: console.error(`Unknown modal type: "${type}"!`); } @@ -212,10 +220,10 @@ class Modal extends React.Component { getColumnLabel={this.props.params.getColumnLabel} overrideLabelsAndGroups={this.props.params.overrideLabelsAndGroups} /> } - { this.props.params.type == 'form-languages' && + { this.props.params.type == MODAL_TYPES.FORM_LANGUAGES && } - { this.props.params.type == 'form-translation-table' && + { this.props.params.type == MODAL_TYPES.FORM_TRANSLATIONS_TABLE && } diff --git a/jsapp/js/constants.es6 b/jsapp/js/constants.es6 index 5886241f7b..560fab4892 100644 --- a/jsapp/js/constants.es6 +++ b/jsapp/js/constants.es6 @@ -7,7 +7,9 @@ const MODAL_TYPES = { ENKETO_PREVIEW: 'enketo-preview', SUBMISSION: 'submission', REPLACE_PROJECT: 'replace-project', - TABLE_COLUMNS: 'table-columns' + TABLE_COLUMNS: 'table-columns', + FORM_LANGUAGES: 'form-languages', + FORM_TRANSLATIONS_TABLE: 'form-translation-table' } const PROJECT_SETTINGS_CONTEXTS = { From 210caa44a4c4efcabb741ba4f97968069fd6cfbc Mon Sep 17 00:00:00 2001 From: pmusaraj Date: Fri, 27 Jul 2018 16:39:01 -0400 Subject: [PATCH 10/95] dismantle translations in form builder, WIP --- jsapp/js/editorMixins/editableForm.es6 | 25 -- jsapp/xlform/src/model.row.coffee | 17 -- jsapp/xlform/src/model.survey.coffee | 16 -- jsapp/xlform/src/view.choices.coffee | 18 +- jsapp/xlform/src/view.row.coffee | 8 - jsapp/xlform/src/view.row.templates.coffee | 2 - .../src/view.surveyApp.templates.coffee | 9 - test/xlform/survey.tests.coffee | 5 +- test/xlform/translations.tests.coffee | 236 +++++++++--------- 9 files changed, 121 insertions(+), 215 deletions(-) diff --git a/jsapp/js/editorMixins/editableForm.es6 b/jsapp/js/editorMixins/editableForm.es6 index 15ea1e79f8..f754be2c91 100644 --- a/jsapp/js/editorMixins/editableForm.es6 +++ b/jsapp/js/editorMixins/editableForm.es6 @@ -176,16 +176,8 @@ export default assign({ stores.allAssets.whenLoaded(uid, (asset) => { this.setState({asset: asset}); - let translations = ( - asset.content && - asset.content.translations && - // slice makes shallow copy - asset.content.translations.slice(0) - ) || []; - this.launchAppForSurveyContent(asset.content, { name: asset.name, - translations: translations, settings__style: asset.settings__style, asset_uid: asset.uid, asset_type: asset.asset_type, @@ -587,12 +579,6 @@ export default assign({ saveButtonText, } = this.buttonStates(); - let translations = this.state.translations || []; - // HACK FIX: filter out weird case of single `null` item array - if (translations.length === 1 && translations[0] === null) { - translations = []; - } - let nameFieldLabel; switch (this.state.asset_type) { case ASSET_TYPES.template.id: @@ -729,17 +715,6 @@ export default assign({ } - { translations.length > 0 && - -

- {translations[0]} - {translations.length > 1 && - {translations[1]} - } -

-
- } - diff --git a/jsapp/xlform/src/model.row.coffee b/jsapp/xlform/src/model.row.coffee index a3c061114e..95b8f0c4f9 100644 --- a/jsapp/xlform/src/model.row.coffee +++ b/jsapp/xlform/src/model.row.coffee @@ -400,23 +400,6 @@ module.exports = do -> return newRow - getTranslatedColumnKey: (col, whichone="primary")-> - if whichone is "_2" - _t = @getSurvey()._translation_2 - else - _t = @getSurvey()._translation_1 - _key = "#{col}" - if _t isnt null - _key += "::#{_t}" - _key - - getLabel: (whichone="primary")-> - _col = @getTranslatedColumnKey("label", whichone) - if _col of @attributes - @getValue _col - else - null - finalize: -> existing_name = @getValue("name") unless existing_name diff --git a/jsapp/xlform/src/model.survey.coffee b/jsapp/xlform/src/model.survey.coffee index 4392a80e09..16ec73a7bd 100644 --- a/jsapp/xlform/src/model.survey.coffee +++ b/jsapp/xlform/src/model.survey.coffee @@ -32,17 +32,6 @@ module.exports = do -> @choices = new $choices.ChoiceLists([], _parent: @) $inputParser.loadChoiceLists(options.choices || [], @choices) - if options.translations - @translations = options.translations - else - @translations = [null] - - if options['_active_translation_name'] - @active_translation_name = options['_active_translation_name'] - - @_translation_1 = @translations[0] - @_translation_2 = @translations[1] - if options.survey if !$inputParser.hasBeenParsed(options) options.survey = $inputParser.parseArr(options.survey) @@ -141,11 +130,6 @@ module.exports = do -> addlSheets = choices: new $choices.ChoiceLists() - if @active_translation_name - obj['#active_translation_name'] = @active_translation_name - - obj.translations = [].concat(@translations) - obj.survey = do => out = [] fn = (r)-> diff --git a/jsapp/xlform/src/view.choices.coffee b/jsapp/xlform/src/view.choices.coffee index f3383a41c4..0cba29433a 100644 --- a/jsapp/xlform/src/view.choices.coffee +++ b/jsapp/xlform/src/view.choices.coffee @@ -20,10 +20,9 @@ module.exports = do -> if cardText.find('.card__buttons__multioptions.js-expand-multioptions').length is 0 cardText.prepend $.parseHTML($viewTemplates.row.expandChoiceList()) @$el.html (@ul = $("
    ", class: @ulClasses)) - _ts = @model.getSurvey().translations if @row.get("type").get("rowType").specifyChoice for option, i in @model.options.models - new OptionView(model: option, cl: @model, translations: _ts).render().$el.appendTo @ul + new OptionView(model: option, cl: @model).render().$el.appendTo @ul if i == 0 while i < 2 @addEmptyOption("Option #{++i}") @@ -55,8 +54,7 @@ module.exports = do -> addEmptyOption: (label)-> emptyOpt = new $choices.Option(label: label) @model.options.add(emptyOpt) - _translations = @model.getSurvey().translations - new OptionView(model: emptyOpt, cl: @model, translations: _translations).render().$el.appendTo @ul + new OptionView(model: emptyOpt, cl: @model).render().$el.appendTo @ul lis = @ul.find('li') if lis.length == 2 lis.find('.js-remove-option').removeClass('hidden') @@ -131,18 +129,6 @@ module.exports = do -> @d.append(@t) @d.append(@c) @$el.html(@d) - - _translation_2 = @options.translations[1] - if _translation_2 isnt undefined - _t_opt = @model.get("label::#{_translation_2}") - if !_t_opt - _no_t = _t("No translation") - _klss = ["card__option-translation", "card__option-translation--empty"].join(" ") - _t_opt = """#{_no_t}""" - $("", {className: 'secondary-translation'}).html(""" - - #{_t_opt} - """).appendTo(@$el) @ keyupinput: (evt)-> ifield = @$("input.inplace_field") diff --git a/jsapp/xlform/src/view.row.coffee b/jsapp/xlform/src/view.row.coffee index caa8c20cbf..63f14c3179 100644 --- a/jsapp/xlform/src/view.row.coffee +++ b/jsapp/xlform/src/view.row.coffee @@ -66,7 +66,6 @@ module.exports = do -> _renderRow: -> @$el.html $viewTemplates.$$render('row.xlfRowView', @surveyView) @$label = @$('.card__header-title') - @$sub_label = @$('.card__header-subtitle') @$card = @$('.card') @$header = @$('.card__header') context = {warnings: []} @@ -77,13 +76,6 @@ module.exports = do -> @listView = new $viewChoices.ListView(model: cl, rowView: @).render() @cardSettingsWrap = @$('.card__settings').eq(0) - _second_translation = @surveyView.survey._translation_2 - if _second_translation isnt undefined - _second_val = @model.getLabel('_2') - if !_second_val - _no_t = _t("No translation") - _second_val = """#{_no_t}""" - @$sub_label.html(_second_val).show() @defaultRowDetailParent = @cardSettingsWrap.find('.card__settings__fields--question-options').eq(0) for [key, val] in @model.attributesArray() when key is 'label' or key is 'type' view = new $viewRowDetail.DetailView(model: val, rowView: @) diff --git a/jsapp/xlform/src/view.row.templates.coffee b/jsapp/xlform/src/view.row.templates.coffee index 2ec0b7f1ee..e3fc36d240 100644 --- a/jsapp/xlform/src/view.row.templates.coffee +++ b/jsapp/xlform/src/view.row.templates.coffee @@ -63,7 +63,6 @@ module.exports = do ->
    -
    @@ -107,7 +106,6 @@ module.exports = do ->
    -
    diff --git a/jsapp/xlform/src/view.surveyApp.templates.coffee b/jsapp/xlform/src/view.surveyApp.templates.coffee index 144f41ab7e..d5c300407e 100644 --- a/jsapp/xlform/src/view.surveyApp.templates.coffee +++ b/jsapp/xlform/src/view.surveyApp.templates.coffee @@ -23,15 +23,6 @@ module.exports = do -> for warning in surveyApp.warnings warnings_html += """

    #{warning}

    """ warnings_html += """
    """ - if survey.translations - t0 = survey._translation_1 - t1 = survey._translation_2 - print_translation = (tx)-> if tx is null then "Unnamed translation" else tx - translations_content = "#{print_translation(t0)}" - if t1 - translations_content += " [#{print_translation(t1)}]" - else - translations_content = "" """ #{warnings_html} diff --git a/test/xlform/survey.tests.coffee b/test/xlform/survey.tests.coffee index ceb66058bd..5ab1929c0f 100644 --- a/test/xlform/survey.tests.coffee +++ b/test/xlform/survey.tests.coffee @@ -136,10 +136,7 @@ do -> 'name': 'no' } ] - }, - 'translations': [ - null - ], + } }) describe 'survey row reordering', -> beforeEach -> diff --git a/test/xlform/translations.tests.coffee b/test/xlform/translations.tests.coffee index da196ba3cf..e09c742b8e 100644 --- a/test/xlform/translations.tests.coffee +++ b/test/xlform/translations.tests.coffee @@ -1,133 +1,133 @@ -{expect} = require('../helper/fauxChai') +# {expect} = require('../helper/fauxChai') -$inputParser = require("../../jsapp/xlform/src/model.inputParser") -$survey = require("../../jsapp/xlform/src/model.survey") +# $inputParser = require("../../jsapp/xlform/src/model.inputParser") +# $survey = require("../../jsapp/xlform/src/model.survey") -describe " translations set proper values ", -> - process = (src)-> - parsed = $inputParser.parse(src) - new $survey.Survey(parsed) +# describe " translations set proper values ", -> +# process = (src)-> +# parsed = $inputParser.parse(src) +# new $survey.Survey(parsed) - it 'example 0', -> - survey1 = process( - survey: [ - type: "text" - label: "VAL1", - name: "val1", - ] - ) - survey2 = process( - survey: [ - type: "text" - label: ["VAL1"], - name: "val1", - ] - translations: [null] - ) +# it 'example 0', -> +# survey1 = process( +# survey: [ +# type: "text" +# label: "VAL1", +# name: "val1", +# ] +# ) +# survey2 = process( +# survey: [ +# type: "text" +# label: ["VAL1"], +# name: "val1", +# ] +# translations: [null] +# ) - expect(survey1._translation_1).toEqual(null) - expect(survey1._translation_2).toEqual(undefined) +# expect(survey1._translation_1).toEqual(null) +# expect(survey1._translation_2).toEqual(undefined) - expect(survey2._translation_1).toEqual(null) - expect(survey2._translation_2).toEqual(undefined) +# expect(survey2._translation_1).toEqual(null) +# expect(survey2._translation_2).toEqual(undefined) - it 'does not have active_translation_name value when none set', -> - survey_json = process( - survey: [type: "text", label: ["VAL1"], name: "val1"] - translations: [null] - ).toJSON() - expect(survey_json['#active_translation_name']).toBeUndefined() +# it 'does not have active_translation_name value when none set', -> +# survey_json = process( +# survey: [type: "text", label: ["VAL1"], name: "val1"] +# translations: [null] +# ).toJSON() +# expect(survey_json['#active_translation_name']).toBeUndefined() - it 'passes thru active_translation_name', -> - survey = process( - survey: [ - type: "text" - label: ["VAL1_NULL", "VAL2_L2"], - name: "val1", - ] - translations: [null, "L2"] - '#active_translation_name': 'XYZ' - ) - expect(survey.active_translation_name).toEqual('XYZ') - _json = survey.toJSON() - expect(_json['#active_translation_name']).toEqual('XYZ') +# it 'passes thru active_translation_name', -> +# survey = process( +# survey: [ +# type: "text" +# label: ["VAL1_NULL", "VAL2_L2"], +# name: "val1", +# ] +# translations: [null, "L2"] +# '#active_translation_name': 'XYZ' +# ) +# expect(survey.active_translation_name).toEqual('XYZ') +# _json = survey.toJSON() +# expect(_json['#active_translation_name']).toEqual('XYZ') - it 'fails with invalid active_translation_name', -> - run = -> - survey = process( - survey: [ - type: "text" - label: ["VAL1_NULL", "VAL2_L2"], - name: "val1", - ] - translations: ["L1", "L2"] - '#active_translation_name': 'XYZ' - ) - # "#active_translation_name" is set, but refers to a value in "translations" - # but in this case there is no null in the translations list so it should - # throw an error - expect(run).toThrow() +# it 'fails with invalid active_translation_name', -> +# run = -> +# survey = process( +# survey: [ +# type: "text" +# label: ["VAL1_NULL", "VAL2_L2"], +# name: "val1", +# ] +# translations: ["L1", "L2"] +# '#active_translation_name': 'XYZ' +# ) +# # "#active_translation_name" is set, but refers to a value in "translations" +# # but in this case there is no null in the translations list so it should +# # throw an error +# expect(run).toThrow() - it 'example 1', -> - survey = process( - survey: [ - type: "text" - label: ["VAL1_NULL", "VAL2_L2"], - name: "val1", - ] - translations: [null, "L2"] - ) - expect(survey._translation_1).toEqual(null) - expect(survey._translation_2).toEqual("L2") - r0 = survey.rows.at(0) - expect(r0.getLabel('_1')).toEqual('VAL1_NULL') - expect(r0.getLabel('_2')).toEqual('VAL2_L2') +# it 'example 1', -> +# survey = process( +# survey: [ +# type: "text" +# label: ["VAL1_NULL", "VAL2_L2"], +# name: "val1", +# ] +# translations: [null, "L2"] +# ) +# expect(survey._translation_1).toEqual(null) +# expect(survey._translation_2).toEqual("L2") +# r0 = survey.rows.at(0) +# expect(r0.getLabel('_1')).toEqual('VAL1_NULL') +# expect(r0.getLabel('_2')).toEqual('VAL2_L2') - rj0 = survey.toJSON().survey[0] - expect(rj0['label']).toBeDefined() - expect(rj0['label::L2']).toBeDefined() +# rj0 = survey.toJSON().survey[0] +# expect(rj0['label']).toBeDefined() +# expect(rj0['label::L2']).toBeDefined() - it 'example 2', -> - survey = process( - survey: [ - type: "text" - label: ["VAL1_L1", "VAL2_L2"], - name: "val1", - ] - translations: ["L1", "L2"] - ) - src = $inputParser.parse( - survey: [ - type: "text" - label: ["VAL1_L1", "VAL2_L2"], - name: "val1", - ] - translations: ["L1", "L2"] - ) - expect(src['_active_translation_name']).toEqual("L1") - expect(src.translations[0]).toEqual(null) +# it 'example 2', -> +# survey = process( +# survey: [ +# type: "text" +# label: ["VAL1_L1", "VAL2_L2"], +# name: "val1", +# ] +# translations: ["L1", "L2"] +# ) +# src = $inputParser.parse( +# survey: [ +# type: "text" +# label: ["VAL1_L1", "VAL2_L2"], +# name: "val1", +# ] +# translations: ["L1", "L2"] +# ) +# expect(src['_active_translation_name']).toEqual("L1") +# expect(src.translations[0]).toEqual(null) - expect(survey._translation_2).toEqual("L2") - _sjson = survey.toJSON() +# expect(survey._translation_2).toEqual("L2") +# _sjson = survey.toJSON() - r0 = survey.rows.at(0) - expect(r0.getLabel('_1')).toEqual('VAL1_L1') - expect(r0.getLabel('_2')).toEqual('VAL2_L2') +# r0 = survey.rows.at(0) +# expect(r0.getLabel('_1')).toEqual('VAL1_L1') +# expect(r0.getLabel('_2')).toEqual('VAL2_L2') - rj0 = _sjson.survey[0] - expect(rj0['label']).toBeDefined() - expect(rj0['label::L2']).toBeDefined() - expect(_sjson['#active_translation_name']).toEqual('L1') +# rj0 = _sjson.survey[0] +# expect(rj0['label']).toBeDefined() +# expect(rj0['label::L2']).toBeDefined() +# expect(_sjson['#active_translation_name']).toEqual('L1') - it 'example 3', -> - run = -> - survey = process( - survey: [ - type: "text" - label: ["VAL1_L2", "VAL2_NULL"], - name: "val1", - ] - translations: ["L2", null] - ) - # run() - expect(run).toThrow('There is an unnamed translation in your form definition') +# it 'example 3', -> +# run = -> +# survey = process( +# survey: [ +# type: "text" +# label: ["VAL1_L2", "VAL2_NULL"], +# name: "val1", +# ] +# translations: ["L2", null] +# ) +# # run() +# expect(run).toThrow('There is an unnamed translation in your form definition') From eaff44476e0ee0be736d86abc38a02daf3f72e8d Mon Sep 17 00:00:00 2001 From: pmusaraj Date: Mon, 30 Jul 2018 10:35:33 -0400 Subject: [PATCH 11/95] fixes to translations table, performance refactoring --- jsapp/js/components/modal.es6 | 5 +- .../modalForms/translationSettings.es6 | 4 +- .../modalForms/translationTable.es6 | 96 +++++++++++-------- jsapp/scss/components/_kobo.modal.scss | 66 +++++-------- 4 files changed, 83 insertions(+), 88 deletions(-) diff --git a/jsapp/js/components/modal.es6 b/jsapp/js/components/modal.es6 index eff7b03d33..9dcbafa9a1 100644 --- a/jsapp/js/components/modal.es6 +++ b/jsapp/js/components/modal.es6 @@ -90,7 +90,10 @@ class Modal extends React.Component { break; case MODAL_TYPES.FORM_TRANSLATIONS_TABLE: - this.setModalTitle(t('Translations table')); + this.setState({ + title: t('Translations table'), + modalClass: 'modal--large' + }); break; default: diff --git a/jsapp/js/components/modalForms/translationSettings.es6 b/jsapp/js/components/modalForms/translationSettings.es6 index 9cfc1bb2aa..05d29c1a1d 100644 --- a/jsapp/js/components/modalForms/translationSettings.es6 +++ b/jsapp/js/components/modalForms/translationSettings.es6 @@ -170,7 +170,6 @@ export class TranslationSettings extends React.Component { } prepareTranslations(content) { - // prepare all translation arrays when adding a language let translated = content.translated, translationsLength = content.translations.length, survey = content.survey, @@ -187,7 +186,7 @@ export class TranslationSettings extends React.Component { } } - // append null values to translations for each survey row + // append null values to translations for choices if (content.choices && content.choices.length) { for (var i = 0, len = choices.length; i < len; i++) { if (choices[i].label.length < translationsLength) { @@ -198,7 +197,6 @@ export class TranslationSettings extends React.Component { return content; } deleteTranslations(content, langIndex) { - // delete items from translation arrays for this langIndex let translated = content.translated, translationsLength = content.translations.length, survey = content.survey, diff --git a/jsapp/js/components/modalForms/translationTable.es6 b/jsapp/js/components/modalForms/translationTable.es6 index 2dbc0ac549..b94552ca3d 100644 --- a/jsapp/js/components/modalForms/translationTable.es6 +++ b/jsapp/js/components/modalForms/translationTable.es6 @@ -1,4 +1,5 @@ import React from 'react' +import ReactTable from 'react-table' import TextareaAutosize from 'react-autosize-textarea' import bem from 'js/bem' @@ -22,10 +23,10 @@ export class TranslationTable extends React.Component { let row = survey[i]; for (var j = 0, len2 = translated.length; j < len2; j++) { var property = translated[j]; - if (row[property]) { + if (row[property] && row[property][0]) { var tableRow = { original: row[property][0], - translation: row[property][props.langIndex], + value: row[property][props.langIndex], name: row.name || row.$autoname, itemProp: property, contentProp: 'survey' @@ -40,24 +41,43 @@ export class TranslationTable extends React.Component { if (choices && choices.length) { for (var i = 0, len = choices.length; i < len; i++) { let choice = choices[i]; - var tableRow = { - original: choice.label[0], - translation: choice.label[props.langIndex], - name: choice.name || choice.$autovalue, - itemProp: 'label', - contentProp: 'choices' + if (choice && choice.label[0]) { + var tableRow = { + original: choice.label[0], + value: choice.label[props.langIndex], + name: choice.name || choice.$autovalue, + itemProp: 'label', + contentProp: 'choices' + } + this.state.tableData.push(tableRow); } - this.state.tableData.push(tableRow); } } + + let translationLabel = props.asset.content.translations[props.langIndex]; + this.columns = [ + { + Header: t('Original string'), + accessor: 'original', + minWidth: 130, + Cell: row => row.original.original + },{ + Header: `${translationLabel} ${t('Translation')}`, + accessor: 'translation', + className: 'translation', + Cell: cellInfo => ( + { + const data = [...this.state.tableData]; + data[cellInfo.index].value = e.target.value; + this.setState({ data }); + }} + value={this.state.tableData[cellInfo.index].value || ''}/> + ) + } + ]; } - onChange(value, index) { - let tD = this.state.tableData; - tD[index].translation = value; - this.setState({ - tableData: tD - }); - } + saveChanges() { var content = this.props.asset.content; let rows = this.state.tableData, @@ -67,8 +87,8 @@ export class TranslationTable extends React.Component { let item = content[rows[i].contentProp].find(o => o.name === rows[i].name || o.$autoname === rows[i].name || o.$autovalue === rows[i].name); var itemProp = rows[i].itemProp; - if (item[itemProp][langIndex] !== rows[i].translation) { - item[itemProp][langIndex] = rows[i].translation; + if (item[itemProp][langIndex] !== rows[i].value) { + item[itemProp][langIndex] = rows[i].value; } } @@ -77,34 +97,26 @@ export class TranslationTable extends React.Component { {content: JSON.stringify(content)} ); } + render () { - let langIndex = this.props.langIndex, - translationLabel = this.props.asset.content.translations[langIndex]; return ( - - - - - - - - - {this.state.tableData.map((item, i)=>{ - return ( - - - - - ); - })} - -
    {t('Original string')}{`${translationLabel} ${t('Translation')}`}
    {item.original} - this.onChange(e.target.value, i)} - value={item.translation || ''}/> -
    + + + {t('Loading...')} + + } + />
    - } - - {this.props.context !== PROJECT_SETTINGS_CONTEXTS.REPLACE && - - } - - - - - - - ); - } - - renderStepChooseTemplate() { - return ( - - - - - {this.renderBackButton()} - - - {this.state.applyTemplateButton} - - - - ); - } - - renderStepUploadFile() { - return ( - - - {t('Import an XLSForm from your computer.')} - - - {!this.state.isUploadFilePending && - - - {t(' Drag and drop the XLSForm file here or click to browse')} - - } - {this.state.isUploadFilePending && -
    - {this.renderLoading(t('Uploading file…'))} -
    - } - - - {this.renderBackButton()} - -
    - ); - } - - renderStepImportUrl() { - return ( - -
    - {t('Enter a valid XLSForm URL in the field below.')}
    - - {t('Having issues? See this help article.')} - -
    - - - - - - - {this.renderBackButton()} - - - {this.state.importUrlButton} - - -
    - ); - } - - renderStepProjectDetails() { - const sectors = session.currentAccount.available_sectors; - const countries = session.currentAccount.available_countries; - - return ( - - {this.props.context === PROJECT_SETTINGS_CONTEXTS.EXISTING && - - - {t('Save Changes')} - - - } - - - {/* form builder displays name in different place */} - {this.props.context !== PROJECT_SETTINGS_CONTEXTS.BUILDER && - - - - - } - - - - - - - - - - - - - - - - - - - - - {(this.props.context === PROJECT_SETTINGS_CONTEXTS.NEW || this.props.context === PROJECT_SETTINGS_CONTEXTS.REPLACE) && - - {/* Don't allow going back if asset already exist */} - {!this.state.formAsset && - this.renderBackButton() - } - - - {this.state.isSubmitPending && t('Please wait…')} - {!this.state.isSubmitPending && this.props.context === PROJECT_SETTINGS_CONTEXTS.NEW && t('Create project')} - {!this.state.isSubmitPending && this.props.context === PROJECT_SETTINGS_CONTEXTS.REPLACE && t('Save')} - - - } - - {this.props.context === PROJECT_SETTINGS_CONTEXTS.EXISTING && this.props.iframeUrl && - -