Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[TASK-624] Allow users to select between translated labels and XML names for displayed form questions in NLP UI #4933

Merged
merged 14 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion jsapp/js/assetUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ export function getOrganizationDisplayString(asset: AssetResponse | ProjectViewA
* Returns the index of language or null if not found.
*/
export function getLanguageIndex(asset: AssetResponse, langString: string) {
let foundIndex = null;
// Return -1 instead of null as that would allow
// `getQuestionOrChoiceDisplayName` to defualt to xml names.
let foundIndex = -1;

if (
Array.isArray(asset.summary?.languages) &&
Expand Down Expand Up @@ -224,6 +226,11 @@ export function getQuestionOrChoiceDisplayName(
}

if (questionOrChoice.label && Array.isArray(questionOrChoice.label)) {
// If the user hasn't made translations yet for a form language show
// the xml names instead of blank.
if (questionOrChoice.label[translationIndex] === null) {
return getRowName(questionOrChoice);
}
return questionOrChoice.label[translationIndex];
} else if (questionOrChoice.label && !Array.isArray(questionOrChoice.label)) {
// in rare cases the label could be a string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export default function ProcessingSidebar(props: ProcessingSidebarProps) {
)}

{displays.includes(StaticDisplays.Data) && (
<SidebarSubmissionData assetContent={props.asset.content} />
<SidebarSubmissionData asset={props.asset} assetContent={props.asset.content} />
duvld marked this conversation as resolved.
Show resolved Hide resolved
)}

{displays.length === 0 && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

.root {
display: flex;
justify-content: right;
justify-content: space-between;
padding-top: sizes.$x18;
padding-bottom: sizes.$x10;
}
Expand Down Expand Up @@ -43,3 +43,7 @@
border-radius: sizes.$x2;
overflow: scroll;
}

.selectWrapper {
width: 40%;
}
duvld marked this conversation as resolved.
Show resolved Hide resolved
17 changes: 17 additions & 0 deletions jsapp/js/components/processing/sidebar/sidebarDisplaySettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import styles from './sidebarDisplaySettings.module.scss';
import MultiCheckbox from 'js/components/common/multiCheckbox';
import type {MultiCheckboxItem} from 'js/components/common/multiCheckbox';
import cx from 'classnames';
import KoboSelect from '../../common/koboSelect';
duvld marked this conversation as resolved.
Show resolved Hide resolved

interface SidebarDisplaySettingsProps {
assetContent: AssetContent | undefined;
Expand Down Expand Up @@ -64,6 +65,7 @@ export default function SidebarDisplaySettings(

const transcript = store.getTranscript();
const availableDisplays = store.getAvailableDisplays(activeTab);
const displayedLanguageList = store.getDisplayedLanguagesList();

function getStaticDisplayText(display: StaticDisplays) {
if (display === StaticDisplays.Transcript) {
Expand Down Expand Up @@ -150,6 +152,21 @@ export default function SidebarDisplaySettings(

return (
<div className={styles.root}>
<div className={styles.selectWrapper}>
<KoboSelect
label={t('Select displayed language')}
name='displayedLanguage'
type='outline'
size='s'
options={displayedLanguageList}
selectedOption={store.getCurrentlyDisplayedLanguage()}
onChange={(languageCode) => {
if (languageCode) {
store.setCurrentlyDisplayedLanguage(languageCode);
}
}}
/>
</div>
duvld marked this conversation as resolved.
Show resolved Hide resolved
<Button
size='m'
type='bare'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import React, {useState} from 'react';
import SubmissionDataList from 'js/components/submissions/submissionDataList';
import singleProcessingStore from 'js/components/processing/singleProcessingStore';
import type {AssetContent} from 'js/dataInterface';
import type {AssetContent, AssetResponse} from 'js/dataInterface';
import {META_QUESTION_TYPES, ADDITIONAL_SUBMISSION_PROPS} from 'js/constants';
import styles from './sidebarSubmissionData.module.scss';

interface SidebarSubmissionDataProps {
assetContent: AssetContent | undefined;
asset: AssetResponse;
}

export default function SidebarSubmissionData(
Expand Down Expand Up @@ -41,7 +42,7 @@ export default function SidebarSubmissionData(
<div className={styles.dataListBody}>
{submissionData && (
<SubmissionDataList
assetContent={props.assetContent}
asset={props.asset}
submissionData={submissionData}
hideQuestions={getQuestionsToHide()}
hideGroups
Expand Down
21 changes: 16 additions & 5 deletions jsapp/js/components/processing/singleProcessingHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React from 'react';
import {QUESTION_TYPES, META_QUESTION_TYPES} from 'js/constants';
import type {AssetContent} from 'js/dataInterface';
import type {AssetContent, AssetResponse} from 'js/dataInterface';
import {
findRowByQpath,
getRowTypeIcon,
getTranslatedRowLabel,
getRowName,
getLanguageIndex,
} from 'js/assetUtils';
import {ROUTES} from 'js/router/routerConstants';
import Button from 'js/components/common/button';
Expand All @@ -22,7 +23,7 @@ import classNames from 'classnames';
interface SingleProcessingHeaderProps extends WithRouterProps {
submissionEditId: string;
assetUid: string;
assetContent: AssetContent;
asset: AssetResponse;
}

interface SingleProcessingHeaderState {
Expand Down Expand Up @@ -86,9 +87,19 @@ class SingleProcessingHeader extends React.Component<
getQuestionSelectorOptions() {
const options: KoboSelectOption[] = [];
const editIds = singleProcessingStore.getSubmissionsEditIds();
const assetContent = this.props.asset.content;
const languageIndex = getLanguageIndex(
this.props.asset,
singleProcessingStore.getCurrentlyDisplayedLanguage()
);

if (!assetContent) {
return [];
}

if (editIds) {
Object.keys(editIds).forEach((qpath) => {
const questionData = findRowByQpath(this.props.assetContent, qpath);
const questionData = findRowByQpath(assetContent, qpath);
// At this point we want to find out whether the question has at least
// one editId (i.e. there is at least one transcriptable response to
// the question). Otherwise there's no point in having the question as
Expand All @@ -107,8 +118,8 @@ class SingleProcessingHeader extends React.Component<
const rowName = getRowName(questionData);
const translatedLabel = getTranslatedRowLabel(
rowName,
this.props.assetContent.survey,
0
assetContent.survey,
languageIndex
);
options.push({
value: qpath,
Expand Down
1 change: 1 addition & 0 deletions jsapp/js/components/processing/singleProcessingRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export default class SingleProcessingRoute extends React.Component<
submissionEditId={this.props.params.submissionEditId}
assetUid={this.props.params.uid}
assetContent={this.state.asset.content}
asset={this.state.asset}
duvld marked this conversation as resolved.
Show resolved Hide resolved
/>
</section>

Expand Down
49 changes: 48 additions & 1 deletion jsapp/js/components/processing/singleProcessingStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
getRowName,
getRowNameByQpath,
getFlatQuestionsList,
getLanguageIndex,
} from 'js/assetUtils';
import type {SurveyFlatPaths} from 'js/assetUtils';
import assetStore from 'js/assetStore';
Expand All @@ -34,6 +35,7 @@ import {
getCurrentProcessingRouteParts,
ProcessingTab,
} from 'js/components/processing/routes.utils';
import type {KoboSelectOption} from '../common/koboSelect';
duvld marked this conversation as resolved.
Show resolved Hide resolved

export enum StaticDisplays {
Data = 'Data',
Expand Down Expand Up @@ -149,6 +151,8 @@ class SingleProcessingStore extends Reflux.Store {

private analysisTabHasUnsavedWork = false;

private currentlyDisplayedLanguage: LanguageCode | string = this.getInitialDisplayedLanguage();
duvld marked this conversation as resolved.
Show resolved Hide resolved

public data: SingleProcessingStoreData = {
translations: [],
isPristine: true,
Expand Down Expand Up @@ -957,6 +961,40 @@ class SingleProcessingStore extends Reflux.Store {
);
}

getDisplayedLanguagesList(): KoboSelectOption[] {
const languagesList = [];

languagesList.push({label: t('XML names'), value: 'xml_names'});
Copy link
Member

Choose a reason for hiding this comment

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

In "Data → Table → Display options" modal, if you have a project with no languages being defined, you will see two options: "XML Values" and "Labels" - let's stick to the same naming, so it's clear these are the same.

Copy link
Member Author

Choose a reason for hiding this comment

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

Tried making the changes for this one and above. Not 100% I understand so lmk if I did it right

const asset = assetStore.getAsset(this.currentAssetUid);
if (asset?.summary?.languages && asset?.summary?.languages.length > 0) {
asset.summary.languages.forEach((language) => {
if (language !== null) {
languagesList.push({
label: language,
value: language,
});
}
});
} else {
languagesList.push({label: t('Default'), value: 'default'});
Copy link
Member

Choose a reason for hiding this comment

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

Could you make it so it works similarly to "Data → Table → Display options" modal? I.e. that you will see the "default" option as being selected? I'm guessing you would need to return 'default' in getInitialDisplayedLanguage instead of empty string. Or change the value: 'default' to value: ''

}

return languagesList;
}

getInitialDisplayedLanguage() {
const asset = assetStore.getAsset(this.currentAssetUid);
if (asset?.summary?.languages && asset?.summary?.languages[0]) {
return asset?.summary?.languages[0];
} else {
return '';
}
}

getCurrentlyDisplayedLanguage() {
return this.currentlyDisplayedLanguage;
}

getInitialDisplays(): SidebarDisplays {
return {
transcript: DefaultDisplays.get(ProcessingTab.Transcript) || [],
Expand Down Expand Up @@ -992,7 +1030,10 @@ class SingleProcessingStore extends Reflux.Store {
const asset = assetStore.getAsset(this.currentAssetUid);

if (asset?.content?.survey) {
const questionsList = getFlatQuestionsList(asset.content.survey, 0)
const questionsList = getFlatQuestionsList(
asset.content.survey,
getLanguageIndex(asset, this.currentlyDisplayedLanguage)
)
.filter((question) => !(question.name === this.currentQuestionName))
.map((question) => {
// We make an object to show the question label to the user but use the
Expand Down Expand Up @@ -1063,6 +1104,12 @@ class SingleProcessingStore extends Reflux.Store {

this.trigger(this.data);
}

setCurrentlyDisplayedLanguage(language: LanguageCode) {
this.currentlyDisplayedLanguage = language;

this.trigger(this.currentlyDisplayedLanguage);
}
}

/**
Expand Down
56 changes: 42 additions & 14 deletions jsapp/js/components/submissions/submissionDataList.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,42 @@
import React from 'react';
import bem, {makeBem} from 'js/bem';
import {getFlatQuestionsList} from 'js/assetUtils';
import {getFlatQuestionsList, getLanguageIndex} from 'js/assetUtils';
import type {FlatQuestion} from 'js/assetUtils';
import type {
AssetContent,
AssetResponse,
SubmissionResponse,
} from 'js/dataInterface';
import {getRowData} from 'js/components/submissions/submissionUtils';
import './submissionDataList.scss';
import singleProcessingStore from '../processing/singleProcessingStore';

bem.SubmissionDataList = makeBem(null, 'submission-data-list', 'ul');
bem.SubmissionDataListQuestion = makeBem(null, 'submission-data-list-question', 'li');
bem.SubmissionDataListQuestion__path = makeBem(bem.SubmissionDataListQuestion, 'path');
bem.SubmissionDataListQuestion__label = makeBem(bem.SubmissionDataListQuestion, 'label', 'h3');
bem.SubmissionDataListQuestion__response = makeBem(bem.SubmissionDataListQuestion, 'response');
bem.SubmissionDataListQuestion = makeBem(
null,
'submission-data-list-question',
'li'
);
bem.SubmissionDataListQuestion__path = makeBem(
bem.SubmissionDataListQuestion,
'path'
);
bem.SubmissionDataListQuestion__label = makeBem(
bem.SubmissionDataListQuestion,
'label',
'h3'
);
bem.SubmissionDataListQuestion__response = makeBem(
bem.SubmissionDataListQuestion,
'response'
);

interface SubmissionDataListProps {
assetContent: AssetContent;
asset: AssetResponse;
submissionData: SubmissionResponse;
/** A list of questions that should be omitted from display. */
hideQuestions?: string[]
hideQuestions?: string[];
/** Whether to display the path (the groups) or not. */
hideGroups?: boolean
hideGroups?: boolean;
}

interface SubmissionDataListState {}
Expand All @@ -36,6 +51,10 @@ export default class SubmissionDataList extends React.Component<
}

renderQuestion(question: FlatQuestion) {
if (!this.props.asset || !this.props.asset.content) {
return null;
}

// check if the question shouldn't be hidden
if (
Array.isArray(this.props.hideQuestions) &&
Expand All @@ -46,17 +65,17 @@ export default class SubmissionDataList extends React.Component<

const response = getRowData(
question.name,
this.props.assetContent.survey || [],
this.props.asset.content.survey || [],
this.props.submissionData
);

return (
<bem.SubmissionDataListQuestion key={question.name}>
{!this.props.hideGroups && question.parents.length >= 1 &&
{!this.props.hideGroups && question.parents.length >= 1 && (
<bem.SubmissionDataListQuestion__path>
{question.parents.join(' / ')}
</bem.SubmissionDataListQuestion__path>
}
)}

<bem.SubmissionDataListQuestion__label>
{question.label}
Expand All @@ -70,11 +89,20 @@ export default class SubmissionDataList extends React.Component<
}

render() {
if (!this.props.assetContent.survey) {
if (!this.props.asset.content || !this.props.asset.content.survey) {
return null;
}

const items = getFlatQuestionsList(this.props.assetContent.survey, 0);
const languageIndex =
getLanguageIndex(
this.props.asset,
singleProcessingStore.getCurrentlyDisplayedLanguage()
);

const items = getFlatQuestionsList(
this.props.asset.content.survey,
languageIndex
);

return (
<bem.SubmissionDataList>
Expand Down
Loading