diff --git a/jsapp/js/account/plans/plan.module.scss b/jsapp/js/account/plans/plan.module.scss index d6be546530..0259b714e2 100644 --- a/jsapp/js/account/plans/plan.module.scss +++ b/jsapp/js/account/plans/plan.module.scss @@ -122,13 +122,8 @@ $plan-badge-height: 38px; padding: 0 0 sizes.$x12 0; } -.priceName { - text-align: left; -} - .priceTitle { color: colors.$kobo-dark-blue; - text-align: left; font-size: sizes.$x20; padding-bottom: sizes.$x16; font-weight: 700; diff --git a/jsapp/js/account/usage/usageProjectBreakdown.module.scss b/jsapp/js/account/usage/usageProjectBreakdown.module.scss index bac00c40e3..e490e1f12d 100644 --- a/jsapp/js/account/usage/usageProjectBreakdown.module.scss +++ b/jsapp/js/account/usage/usageProjectBreakdown.module.scss @@ -50,7 +50,7 @@ font-weight: 700; font-size: sizes.$x13; color: colors.$kobo-gray-40; - text-align: left; + text-align: initial; padding-block: 1.5%; } diff --git a/jsapp/js/account/usage/usageProjectBreakdown.tsx b/jsapp/js/account/usage/usageProjectBreakdown.tsx index bd86aeb7f9..bf5fc9b202 100644 --- a/jsapp/js/account/usage/usageProjectBreakdown.tsx +++ b/jsapp/js/account/usage/usageProjectBreakdown.tsx @@ -143,7 +143,7 @@ const ProjectBreakdown = () => { return ( - + { - + {this.props.asset.settings && this.props.asset.tag_string && this.props.asset.tag_string.length > 0 && diff --git a/jsapp/js/components/common/button.scss b/jsapp/js/components/common/button.scss index 53d6dee635..ecea31c3b3 100644 --- a/jsapp/js/components/common/button.scss +++ b/jsapp/js/components/common/button.scss @@ -155,7 +155,7 @@ $button-border-radius: sizes.$x6; position: relative; // Needed for tooltips, pending state etc. font-weight: 500; text-decoration: none; - text-align: left; + text-align: initial; padding: 0; margin: 0; border-width: $button-border-width; diff --git a/jsapp/js/components/common/checkbox.scss b/jsapp/js/components/common/checkbox.scss index 81db7c690f..0733c89b32 100644 --- a/jsapp/js/components/common/checkbox.scss +++ b/jsapp/js/components/common/checkbox.scss @@ -62,7 +62,7 @@ } .checkbox__input + .checkbox__label { - margin-left: sizes.$x6; + margin-inline-start: sizes.$x6; } .checkbox__input { diff --git a/jsapp/js/components/common/inlineMessage.scss b/jsapp/js/components/common/inlineMessage.scss index 1d922ad531..669cfa6ac3 100644 --- a/jsapp/js/components/common/inlineMessage.scss +++ b/jsapp/js/components/common/inlineMessage.scss @@ -54,7 +54,7 @@ // We need a bit stronger specificity here .k-inline-message p.k-inline-message__message { margin: 0; - text-align: left; + text-align: initial; font-size: sizes.$x14; line-height: inherit; } diff --git a/jsapp/js/components/common/koboSelect.scss b/jsapp/js/components/common/koboSelect.scss index 95ae44e9b6..b7f198eb71 100644 --- a/jsapp/js/components/common/koboSelect.scss +++ b/jsapp/js/components/common/koboSelect.scss @@ -29,7 +29,7 @@ $k-select-menu-padding: sizes.$x6; @include mixins.centerRowFlex; justify-content: space-between; font-weight: 400; - text-align: left; + text-align: initial; border-width: button.$button-border-width; border-style: solid; border-color: transparent; @@ -117,7 +117,7 @@ $k-select-menu-padding: sizes.$x6; height: $k-select-option-height; color: colors.$kobo-gray-40; padding: 0 #{sizes.$x16 - sizes.$x2}; - text-align: left; + text-align: initial; } .k-select__menu-message { diff --git a/jsapp/js/components/common/koboSelect.tsx b/jsapp/js/components/common/koboSelect.tsx index a66b1b87d0..83d5a2dbe4 100644 --- a/jsapp/js/components/common/koboSelect.tsx +++ b/jsapp/js/components/common/koboSelect.tsx @@ -11,6 +11,7 @@ import {ButtonToIconMap} from 'js/components/common/button'; import KoboDropdown from 'js/components/common/koboDropdown'; import koboDropdownActions from 'js/components/common/koboDropdownActions'; import './koboSelect.scss'; +import type {KoboDropdownPlacement} from 'js/components/common/koboDropdown'; // We can't use "kobo-select" as it is already being used for custom styling of `react-select`. bem.KoboSelect = makeBem(null, 'k-select'); @@ -57,6 +58,7 @@ interface KoboSelectProps { * Sizes are generally the same as in button component so we use same type. */ size: ButtonSize; + placement?: KoboDropdownPlacement; /** Without this option select always need the `selectedOption`. */ isClearable?: boolean; /** This option displays a text box filtering options when opened. */ @@ -190,7 +192,7 @@ class KoboSelect extends React.Component { if (foundSelectedOption) { return ( - + {foundSelectedOption.icon && { this.props.selectedOption === option.value ), }} + dir='auto' > {option.icon && } @@ -348,7 +351,7 @@ class KoboSelect extends React.Component { { const inputProps: InnerInputProps = { placeholder: this.props.placeholder || DEFAULT_PLACEHOLDER, 'data-cy': this.props['data-cy'], + dir: 'auto', }; if (this.props.label) { diff --git a/jsapp/js/components/common/loadingSpinner.module.scss b/jsapp/js/components/common/loadingSpinner.module.scss index da81c1ef41..33ce58f262 100644 --- a/jsapp/js/components/common/loadingSpinner.module.scss +++ b/jsapp/js/components/common/loadingSpinner.module.scss @@ -50,7 +50,7 @@ max-height: 300px; overflow: auto; word-wrap: break-word; - text-align: left; + text-align: initial; } } diff --git a/jsapp/js/components/common/modal.scss b/jsapp/js/components/common/modal.scss index 8a7db2e317..c3e09d6d0f 100644 --- a/jsapp/js/components/common/modal.scss +++ b/jsapp/js/components/common/modal.scss @@ -353,7 +353,7 @@ $modal-custom-header-height: sizes.$x60; .intro { margin-bottom: 20px; - text-align: left; + text-align: initial; } .form-modal__item[disabled] { diff --git a/jsapp/js/components/common/multiCheckbox.scss b/jsapp/js/components/common/multiCheckbox.scss index 509b5e00ce..0306bd7bbb 100644 --- a/jsapp/js/components/common/multiCheckbox.scss +++ b/jsapp/js/components/common/multiCheckbox.scss @@ -6,6 +6,8 @@ height: auto; width: 100%; overflow: hidden auto; + text-align: initial; + padding: 0; } .multi-checkbox__item:not(:last-child) { diff --git a/jsapp/js/components/common/multiCheckbox.tsx b/jsapp/js/components/common/multiCheckbox.tsx index fb3fe6d7a4..67c6ea16c8 100644 --- a/jsapp/js/components/common/multiCheckbox.tsx +++ b/jsapp/js/components/common/multiCheckbox.tsx @@ -43,6 +43,7 @@ export default function MultiCheckbox (props: MultiCheckboxProps) { {props.items.map((item, itemIndex) => ( diff --git a/jsapp/js/components/common/radio.scss b/jsapp/js/components/common/radio.scss index b51cb9891c..02ea3fbeab 100644 --- a/jsapp/js/components/common/radio.scss +++ b/jsapp/js/components/common/radio.scss @@ -30,7 +30,7 @@ } .radio__input + .radio__label { - margin-left: sizes.$x6; + margin-inline-start: sizes.$x6; } .radio__input { diff --git a/jsapp/js/components/common/textBox.tsx b/jsapp/js/components/common/textBox.tsx index 41f98456ec..6a22e451b8 100644 --- a/jsapp/js/components/common/textBox.tsx +++ b/jsapp/js/components/common/textBox.tsx @@ -182,6 +182,8 @@ export default function TextBox(props: TextBoxProps) { // For `number` type we allow only positive integers step: props.type === 'number' ? 1 : undefined, min: props.type === 'number' ? 0 : undefined, + // All textboxes handles text direction of user content with browser + // built-in functionality dir: 'auto', }; diff --git a/jsapp/js/components/common/toggleSwitch.scss b/jsapp/js/components/common/toggleSwitch.scss index 418e8d8727..f13934b127 100644 --- a/jsapp/js/components/common/toggleSwitch.scss +++ b/jsapp/js/components/common/toggleSwitch.scss @@ -47,7 +47,8 @@ } .toggle-switch__slider + .toggle-switch__label { - margin-left: 6px; + // For RTL content + margin-inline-start: 6px; } input:checked + .toggle-switch__slider { diff --git a/jsapp/js/components/formSummary.scss b/jsapp/js/components/formSummary.scss index c7c8527433..131fb9821f 100644 --- a/jsapp/js/components/formSummary.scss +++ b/jsapp/js/components/formSummary.scss @@ -86,7 +86,7 @@ color: colors.$kobo-gray-40; border-bottom: 1px solid colors.$kobo-gray-92; position: relative; - text-align: left; + text-align: initial; &:last-child { border-bottom: none; diff --git a/jsapp/js/components/formSummaryProjectInfo.tsx b/jsapp/js/components/formSummaryProjectInfo.tsx index 1fbaa3e04f..2c77e2f298 100644 --- a/jsapp/js/components/formSummaryProjectInfo.tsx +++ b/jsapp/js/components/formSummaryProjectInfo.tsx @@ -69,11 +69,13 @@ export default function FormSummaryProjectInfo( {metadata.description && ( {/* description - takes whole row */} - + {metadata.description?.label ?? t('Description')} - {props.asset.settings.description || '-'} +
+ {props.asset.settings.description || '-'} +
)} diff --git a/jsapp/js/components/header/headerTitleEditor.tsx b/jsapp/js/components/header/headerTitleEditor.tsx index 9d0f852762..68c50d65bf 100644 --- a/jsapp/js/components/header/headerTitleEditor.tsx +++ b/jsapp/js/components/header/headerTitleEditor.tsx @@ -121,6 +121,7 @@ class HeaderTitleEditor extends React.Component< return ( - + {items.map(this.renderQuestion.bind(this))} diff --git a/jsapp/js/components/library/assetInfoBox.tsx b/jsapp/js/components/library/assetInfoBox.tsx index e0c85ccf95..74c4001238 100644 --- a/jsapp/js/components/library/assetInfoBox.tsx +++ b/jsapp/js/components/library/assetInfoBox.tsx @@ -96,7 +96,9 @@ export default class AssetInfoBox extends React.Component< {this.state.areDetailsVisible && - {this.props.asset.settings.description || '-'} +
+ {this.props.asset.settings.description || '-'} +
} diff --git a/jsapp/js/components/map.scss b/jsapp/js/components/map.scss index 035653ae65..2595fc7e05 100644 --- a/jsapp/js/components/map.scss +++ b/jsapp/js/components/map.scss @@ -114,7 +114,6 @@ border-radius: 6px; padding: 12px; - text-align: left; color: colors.$kobo-gray-40; } diff --git a/jsapp/js/components/modalForms/bulkEditSubmissionsForm.es6 b/jsapp/js/components/modalForms/bulkEditSubmissionsForm.es6 index 192e79f5ed..db8832fdeb 100644 --- a/jsapp/js/components/modalForms/bulkEditSubmissionsForm.es6 +++ b/jsapp/js/components/modalForms/bulkEditSubmissionsForm.es6 @@ -238,7 +238,7 @@ class BulkEditSubmissionsForm extends React.Component { {renderQuestionTypeIcon(question.type)} - + {question.parents.length > 0 && {question.parents.join(' / ') + ' /'} } @@ -556,7 +556,7 @@ class BulkEditRowForm extends React.Component { return ( - + {renderQuestionTypeIcon(this.props.question.type)} diff --git a/jsapp/js/components/modalForms/languageForm.es6 b/jsapp/js/components/modalForms/languageForm.es6 index 858b9b8e68..a4fc3c8344 100644 --- a/jsapp/js/components/modalForms/languageForm.es6 +++ b/jsapp/js/components/modalForms/languageForm.es6 @@ -13,6 +13,7 @@ Properties: - onLanguageChange : required - existingLanguages : for validation purposes - isDefault : for default language only +- isPending : marks the submit button as pending */ class LanguageForm extends React.Component { constructor(props) { @@ -43,6 +44,7 @@ class LanguageForm extends React.Component { autoBind(this); } + isLanguageNameValid() { if (this.props.existingLanguages) { let isNameUnique = true; @@ -61,6 +63,7 @@ class LanguageForm extends React.Component { return true; } } + isLanguageCodeValid() { if (this.props.existingLanguages) { let isCodeUnique = true; @@ -79,14 +82,13 @@ class LanguageForm extends React.Component { return true; } } + onSubmit(evt) { evt.preventDefault(); - evt.currentTarget.disabled = true; const isNameValid = this.isLanguageNameValid(); if (!isNameValid) { this.setState({nameError: t('Name must be unique!')}); - evt.currentTarget.disabled = false; } else { this.setState({nameError: null}); } @@ -94,7 +96,6 @@ class LanguageForm extends React.Component { const isCodeValid = this.isLanguageCodeValid(); if (!isCodeValid) { this.setState({codeError: t('Code must be unique!')}); - evt.currentTarget.disabled = false; } else { this.setState({codeError: null}); } @@ -110,14 +111,24 @@ class LanguageForm extends React.Component { }, langIndex); } } + onNameChange(newName) { - this.setState({name: toTitleCase(newName.trim().toLowerCase())}); + this.setState({ + name: toTitleCase(newName.trim().toLowerCase()), + nameError: null, + }); } + onCodeChange(newCode) { - this.setState({code: newCode.trim().toLowerCase()}); + this.setState({ + code: newCode.trim().toLowerCase(), + codeError: null, + }); } + render() { - let isAnyFieldEmpty = this.state.name.length === 0 || this.state.code.length === 0; + const isAnyFieldEmpty = this.state.name.length === 0 || this.state.code.length === 0; + const hasErrors = this.state.nameError !== null || this.state.codeError !== null; return ( @@ -126,7 +137,7 @@ class LanguageForm extends React.Component { @@ -137,7 +148,7 @@ class LanguageForm extends React.Component { @@ -148,10 +159,11 @@ class LanguageForm extends React.Component { type='full' color='blue' size='l' - onClick={this.onSubmit.bind(this)} + label={this.props.langIndex !== undefined ? t('Update') : (this.props.isDefault) ? t('Set') : t('Add')} isSubmit + isPending={this.props.isPending} isDisabled={isAnyFieldEmpty} - label={this.props.langIndex !== undefined ? t('Update') : (this.props.isDefault) ? t('Set') : t('Add')} + onClick={this.onSubmit.bind(this)} /> diff --git a/jsapp/js/components/modalForms/translationSettings.es6 b/jsapp/js/components/modalForms/translationSettings.es6 index 48cfb4e11b..2ff927e230 100644 --- a/jsapp/js/components/modalForms/translationSettings.es6 +++ b/jsapp/js/components/modalForms/translationSettings.es6 @@ -42,7 +42,7 @@ export class TranslationSettings extends React.Component { asset: props.asset, translations: translations, showAddLanguageForm: false, - isUpdatingDefaultLanguage: false, + isUpdatingAsset: false, renameLanguageIndex: -1, }; autoBind(this); @@ -68,7 +68,7 @@ export class TranslationSettings extends React.Component { asset: asset, translations: asset.content.translations || [], showAddLanguageForm: false, - isUpdatingDefaultLanguage: false, + isUpdatingAsset: false, renameLanguageIndex: -1, }); @@ -233,7 +233,6 @@ export class TranslationSettings extends React.Component { ).replace('##lang##', escapeHtml(langString)), labels: {ok: t('Confirm'), cancel: t('Cancel')}, onok: () => { - this.setState({isUpdatingDefaultLanguage: true}); const content = this.state.asset.content; content.settings.default_language = langString; this.updateAsset(content); @@ -244,6 +243,8 @@ export class TranslationSettings extends React.Component { dialog.set(opts).show(); } updateAsset(content) { + this.setState({isUpdatingAsset: true}); + actions.resources.updateAsset( this.state.asset.uid, {content: JSON.stringify(content)}, @@ -321,7 +322,8 @@ export class TranslationSettings extends React.Component { @@ -364,7 +366,7 @@ export class TranslationSettings extends React.Component { size='m' onClick={() => {this.changeDefaultLanguage(i);}} isDisabled={ - this.state.isUpdatingDefaultLanguage || + this.state.isUpdatingAsset || !this.canEditLanguages() } tooltip={t('Make default')} @@ -380,7 +382,7 @@ export class TranslationSettings extends React.Component { size='m' onClick={() => {this.toggleRenameLanguageForm(i);}} isDisabled={ - this.state.isUpdatingDefaultLanguage || + this.state.isUpdatingAsset || !this.canEditLanguages() } startIcon={this.state.renameLanguageIndex === i ? 'close': 'edit'} @@ -398,7 +400,7 @@ export class TranslationSettings extends React.Component { this.state.translations[i] ); }} - isDisabled={this.state.isUpdatingDefaultLanguage} + isDisabled={this.state.isUpdatingAsset} startIcon='language-settings' tooltip={t('Update translations')} tooltipPosition='right' @@ -411,7 +413,7 @@ export class TranslationSettings extends React.Component { size='m' onClick={() => {this.deleteLanguage(i);}} isDisabled={ - this.state.isUpdatingDefaultLanguage || + this.state.isUpdatingAsset || !this.canEditLanguages() } startIcon='trash' @@ -425,9 +427,10 @@ export class TranslationSettings extends React.Component { {this.state.renameLanguageIndex === i && ( @@ -463,7 +466,8 @@ export class TranslationSettings extends React.Component {
diff --git a/jsapp/js/components/modalForms/translationTable.es6 b/jsapp/js/components/modalForms/translationTable.es6 index 4ac7aeceb6..e8186a8ec3 100644 --- a/jsapp/js/components/modalForms/translationTable.es6 +++ b/jsapp/js/components/modalForms/translationTable.es6 @@ -142,6 +142,7 @@ export class TranslationTable extends React.Component { }} value={this.state.tableData[cellInfo.index].value || ''} disabled={cellInfo.original.isLabelLocked} + dir='auto' /> ); }, @@ -334,6 +335,8 @@ export class TranslationTable extends React.Component { nextText={t('Next')} minRows={1} loadingText={} + // Enables RTL support in table cells + getTdProps={() => ({dir: 'auto'})} /> diff --git a/jsapp/js/components/modals/koboModal.scss b/jsapp/js/components/modals/koboModal.scss index f622c061b3..e3ab12c87e 100644 --- a/jsapp/js/components/modals/koboModal.scss +++ b/jsapp/js/components/modals/koboModal.scss @@ -79,11 +79,13 @@ $kobo-modal-header-icon-margin: sizes.$x10; h1 { @include mixins.centerRowFlex; + flex: 1; color: colors.$kobo-gray-24; font-size: sizes.$x18; font-weight: 700; margin: 0; line-height: 1; + margin-inline-end: 10px; // If there is close button, we need to limit the width (for a long content) &:not(:only-child) { diff --git a/jsapp/js/components/permissions/copyTeamPermissions.component.tsx b/jsapp/js/components/permissions/copyTeamPermissions.component.tsx index ae4ad4aa34..9062827bab 100644 --- a/jsapp/js/components/permissions/copyTeamPermissions.component.tsx +++ b/jsapp/js/components/permissions/copyTeamPermissions.component.tsx @@ -1,7 +1,6 @@ import React from 'react'; import bem from 'js/bem'; import classNames from 'classnames'; -import Select from 'react-select'; import type {SingleValue} from 'react-select'; import alertify from 'alertifyjs'; import {stores} from '../../stores'; @@ -10,11 +9,8 @@ import type {AssetStoreData} from 'js/assetStore'; import {actions} from '../../actions'; import {notify, escapeHtml} from 'js/utils'; import Button from 'js/components/common/button'; - -interface ReactSelectOption { - value: string; - label: string | null; -} +import KoboSelect from 'js/components/common/koboSelect'; +import type {KoboSelectOption} from 'js/components/common/koboSelect'; interface CopyTeamPermissionsProps { assetUid: string; @@ -69,24 +65,11 @@ export default class CopyTeamPermissions extends React.Component< this.setState({isCopyFormVisible: !this.state.isCopyFormVisible}); } - getSelectedProjectOption() { - if (this.state.sourceUid) { - return { - value: this.state.sourceUid, - label: this.state.sourceName, - }; - } else { - return false; - } - } - - onSelectedProjectChange( - option: SingleValue - ) { - if (option !== null && typeof option !== 'boolean') { + onSelectedProjectChange(newSelectedOption: string | null) { + if (newSelectedOption !== null) { this.setState({ - sourceUid: option.value, - sourceName: stores.allAssets.byUid[option.value].name, + sourceUid: newSelectedOption, + sourceName: stores.allAssets.byUid[newSelectedOption].name, }); } } @@ -132,7 +115,7 @@ export default class CopyTeamPermissions extends React.Component< const isImportButtonEnabled = this.state.sourceUid !== null && !this.state.isAwaitingAssetChange; - const availableOptions: ReactSelectOption[] = []; + const availableOptions: KoboSelectOption[] = []; for (const assetUid in stores.allAssets.byUid) { if (stores.allAssets.byUid.hasOwnProperty(assetUid)) { // because choosing itself doesn't make sense @@ -172,16 +155,16 @@ export default class CopyTeamPermissions extends React.Component< - + data-type='label' data-kuid={ch} dir="auto" />