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<
-
|
))}
diff --git a/jsapp/js/components/reports/reportViewItem.component.tsx b/jsapp/js/components/reports/reportViewItem.component.tsx
index f4e4c4b0f6..5c49e59713 100644
--- a/jsapp/js/components/reports/reportViewItem.component.tsx
+++ b/jsapp/js/components/reports/reportViewItem.component.tsx
@@ -282,7 +282,7 @@ class ReportViewItem extends React.Component