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

Fix replaceSupportEmail function #4689

Merged
merged 9 commits into from
Nov 1, 2023
Merged
3 changes: 2 additions & 1 deletion jsapp/js/account/accountSettingsRoute.tsx
Expand Up @@ -6,7 +6,8 @@ import sessionStore from 'js/stores/session';
import './accountSettings.scss';
import Checkbox from '../components/common/checkbox';
import TextBox from '../components/common/textBox';
import {addRequiredToLabel, notify, stringToColor} from '../utils';
import {notify, stringToColor} from 'js/utils';
import {addRequiredToLabel} from 'js/textUtils';
import envStore from '../envStore';
import WrappedSelect from '../components/common/wrappedSelect';
import {dataInterface} from '../dataInterface';
Expand Down
2 changes: 1 addition & 1 deletion jsapp/js/account/subscriptionStore.ts
@@ -1,5 +1,5 @@
import {makeAutoObservable} from 'mobx';
import {handleApiFail} from 'js/utils';
import {handleApiFail} from 'js/api';
import {ROOT_URL} from 'js/constants';
import {fetchGet} from 'jsapp/js/api';
import type {PaginatedResponse} from 'js/dataInterface';
Expand Down
12 changes: 0 additions & 12 deletions jsapp/js/actions.d.ts
Expand Up @@ -42,17 +42,6 @@ interface GetProcessingSubmissionsCompletedDefinition extends Function {
listen: (callback: (response: GetProcessingSubmissionsResponse) => void) => Function;
}

interface GetEnvironmentDefinition extends Function {
(): void;
completed: GetEnvironmentCompletedDefinition;
failed: GenericFailedDefinition;
}

interface GetEnvironmentCompletedDefinition extends Function {
(response: EnvironmentResponse): void;
listen: (callback: (response: EnvironmentResponse) => void) => Function;
}

interface LoadAssetDefinition extends Function {
(params: {id: string}): void;
completed: LoadAssetCompletedDefinition;
Expand Down Expand Up @@ -107,7 +96,6 @@ export namespace actions {
routeUpdate: GenericCallbackDefinition;
};
const auth: {
getEnvironment: GetEnvironmentDefinition;
verifyLogin: {
loggedin: GenericCallbackDefinition;
};
Expand Down
18 changes: 2 additions & 16 deletions jsapp/js/actions.es6
Expand Up @@ -17,10 +17,8 @@ import submissionsActions from './actions/submissions';
import formMediaActions from './actions/mediaActions';
import exportsActions from './actions/exportsActions';
import dataShareActions from './actions/dataShareActions';
import {
notify,
replaceSupportEmail,
} from 'utils';
import {notify} from 'js/utils';
import {replaceSupportEmail} from 'js/textUtils';

// Configure Reflux
Reflux.use(RefluxPromise(window.Promise));
Expand All @@ -45,7 +43,6 @@ actions.auth = Reflux.createActions({
verifyLogin: {children: ['loggedin', 'anonymous', 'failed']},
logout: {children: ['completed', 'failed']},
changePassword: {children: ['completed', 'failed']},
getEnvironment: {children: ['completed', 'failed']},
getApiToken: {children: ['completed', 'failed']},
});

Expand Down Expand Up @@ -466,17 +463,6 @@ actions.auth.changePassword.failed.listen(() => {
notify(t('failed to change password'), 'error');
});

actions.auth.getEnvironment.listen(function(){
dataInterface.environment()
.done((data)=>{
actions.auth.getEnvironment.completed(data);
})
.fail(actions.auth.getEnvironment.failed);
});
actions.auth.getEnvironment.failed.listen(() => {
notify(t('failed to load environment data'), 'error');
});

actions.auth.getApiToken.listen(() => {
dataInterface.apiToken()
.done((response) => {
Expand Down
47 changes: 46 additions & 1 deletion jsapp/js/api.ts
Expand Up @@ -2,7 +2,52 @@
import {ROOT_URL} from './constants';
import type {Json} from './components/common/common.interfaces';
import type {FailResponse} from 'js/dataInterface';
import {handleApiFail} from 'js/utils';
import {notify} from './utils';

/**
* Useful for handling the fail responses from API. Its main goal is to display
* a helpful error toast notification and to pass the error message to Raven.
*
* It can detect if we got HTML string as response and uses a generic message
* instead of spitting it out.
*/
export function handleApiFail(response: FailResponse) {
// Avoid displaying toast when purposefuly aborted a request
if (response.status === 0 && response.statusText === 'abort') {
return;
}

let message = response.responseText;

// Detect if response is HTML code string
if (
typeof message === 'string' &&
message.includes('</html>') &&
message.includes('</body>')
) {
// Try plucking the useful error message from the HTML string - this works
// for Werkzeug Debugger only. It is being used on development environment,
// on production this would most probably result in undefined message (and
// thus falling back to the generic message below).
const htmlDoc = new DOMParser().parseFromString(message, 'text/html');
message = htmlDoc.getElementsByClassName('errormsg')?.[0]?.innerHTML;
}

if (!message) {
message = t('An error occurred');
if (response.status || response.statusText) {
message += ` — ${response.status} ${response.statusText}`;
} else if (!window.navigator.onLine) {
// another general case — the original fetch response.message might have
// something more useful to say.
message += ' — ' + t('Your connection is offline');
}
}

notify.error(message);

window.Raven?.captureMessage(message);
}

const JSON_HEADER = 'application/json';

Expand Down
2 changes: 1 addition & 1 deletion jsapp/js/components/common/assetName.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import {getAssetDisplayName} from 'js/assetUtils';
import {hasLongWords} from 'js/utils';
import {hasLongWords} from 'js/textUtils';
import type {AssetResponse, ProjectViewAsset} from 'js/dataInterface';
import './assetName.scss';

Expand Down
2 changes: 1 addition & 1 deletion jsapp/js/components/languages/languagesListStore.ts
Expand Up @@ -3,7 +3,7 @@ import type {
PaginatedResponse,
FailResponse,
} from 'js/dataInterface';
import {handleApiFail} from 'js/utils';
import {handleApiFail} from 'js/api';
import {ROOT_URL} from 'js/constants';
import languagesStore from './languagesStore';
import type {ListLanguage} from './languagesStore';
Expand Down
6 changes: 2 additions & 4 deletions jsapp/js/components/modalForms/languageForm.es6
Expand Up @@ -2,10 +2,8 @@ import React from 'react';
import autoBind from 'react-autobind';
import bem from 'js/bem';
import TextBox from 'js/components/common/textBox';
import {
getLangAsObject,
toTitleCase
} from 'utils';
import {getLangAsObject} from 'js/utils';
import {toTitleCase} from 'js/textUtils';

/*
Properties:
Expand Down
4 changes: 2 additions & 2 deletions jsapp/js/components/modalForms/projectSettings.es6
Expand Up @@ -19,13 +19,13 @@ import TemplatesList from 'js/components/templatesList';
import {actions} from 'js/actions';
import {dataInterface} from 'js/dataInterface';
import {
addRequiredToLabel,
escapeHtml,
isAValidUrl,
validFileTypes,
notify,
join,
} from 'utils';
} from 'js/utils';
import {addRequiredToLabel} from 'js/textUtils';
import {
NAME_MAX_LENGTH,
PROJECT_SETTINGS_CONTEXTS,
Expand Down
6 changes: 2 additions & 4 deletions jsapp/js/components/permissions/permValidator.es6
Expand Up @@ -5,10 +5,8 @@ import autoBind from 'react-autobind';
import permConfig from './permConfig';
import {actions} from '../../actions';
import union from 'lodash.union';
import {
notify,
replaceSupportEmail
} from 'utils';
import {notify} from 'js/utils';
import {replaceSupportEmail} from 'js/textUtils';

const INVALID_PERMS_ERROR = t('The stored permissions are invalid. Please assign them again. If this problem persists, contact help@kobotoolbox.org');

Expand Down
6 changes: 2 additions & 4 deletions jsapp/js/components/permissions/sharingForm.es6
Expand Up @@ -11,10 +11,8 @@ import {actions} from 'js/actions';
import bem from 'js/bem';
import LoadingSpinner from 'js/components/common/loadingSpinner';
import InlineMessage from 'js/components/common/inlineMessage';
import {
buildUserUrl,
replaceBracketsWithLink,
} from 'utils';
import {buildUserUrl} from 'js/utils';
import {replaceBracketsWithLink} from 'js/textUtils';
import {
ASSET_TYPES,
ANON_USERNAME,
Expand Down
2 changes: 1 addition & 1 deletion jsapp/js/components/support/helpBubbleStore.ts
@@ -1,7 +1,7 @@
import throttle from 'lodash.throttle';
import {makeAutoObservable, when} from 'mobx';
import type {PaginatedResponse, FailResponse} from 'js/dataInterface';
import {handleApiFail} from 'js/utils';
import {handleApiFail} from 'js/api';
import {ROOT_URL} from 'js/constants';
import sessionStore from 'js/stores/session';

Expand Down
37 changes: 0 additions & 37 deletions jsapp/js/dataInterface.ts
Expand Up @@ -735,39 +735,6 @@ export interface TransxLanguages {
};
}

export interface EnvironmentResponse {
mfa_has_availability_list: boolean;
terms_of_service_url: string;
privacy_policy_url: string;
source_code_url: string;
support_email: string;
support_url: string;
community_url: string;
project_metadata_fields: EnvStoreFieldItem[];
user_metadata_fields: EnvStoreFieldItem[];
sector_choices: string[][];
operational_purpose_choices: string[][];
country_choices: string[][];
interface_languages: string[][];
transcription_languages: TransxLanguages;
translation_languages: TransxLanguages;
submission_placeholder: string;
frontend_min_retry_time: number;
frontend_max_retry_time: number;
asr_mt_features_enabled: boolean;
mfa_localized_help_text: string;
mfa_enabled: boolean;
mfa_per_user_availability: boolean;
mfa_code_length: number;
stripe_public_key: string | null;
social_apps: SocialApp[];
free_tier_thresholds: FreeTierThresholds;
free_tier_display: FreeTierDisplay;
enable_custom_password_guidance_text: boolean;
custom_password_localized_help_text: string;
enable_password_entropy_meter: boolean;
}

export interface AssetSubscriptionsResponse {
/** url of subscription */
url: string;
Expand Down Expand Up @@ -1782,8 +1749,4 @@ export const dataInterface: DataInterface = {
data: data,
});
},

environment(): JQuery.jqXHR<EnvironmentResponse> {
return $ajax({url: `${ROOT_URL}/environment/`});
},
};
79 changes: 66 additions & 13 deletions jsapp/js/envStore.ts
@@ -1,10 +1,45 @@
import {actions} from 'js/actions';
import type {
LabelValuePair,
TransxLanguages,
EnvironmentResponse,
FailResponse,
} from 'js/dataInterface';
import {makeAutoObservable} from 'mobx';
import {fetchGet, handleApiFail} from 'js/api';

const ENV_ENDPOINT = '/environment/';

interface EnvironmentResponse {
mfa_has_availability_list: boolean;
terms_of_service_url: string;
privacy_policy_url: string;
source_code_url: string;
support_email: string;
support_url: string;
community_url: string;
project_metadata_fields: EnvStoreFieldItem[];
user_metadata_fields: EnvStoreFieldItem[];
sector_choices: string[][];
operational_purpose_choices: string[][];
country_choices: string[][];
interface_languages: string[][];
transcription_languages: TransxLanguages;
translation_languages: TransxLanguages;
submission_placeholder: string;
frontend_min_retry_time: number;
frontend_max_retry_time: number;
asr_mt_features_enabled: boolean;
mfa_localized_help_text: string;
mfa_enabled: boolean;
mfa_per_user_availability: boolean;
mfa_code_length: number;
stripe_public_key: string | null;
social_apps: SocialApp[];
free_tier_thresholds: FreeTierThresholds;
free_tier_display: FreeTierDisplay;
enable_custom_password_guidance_text: boolean;
custom_password_localized_help_text: string;
enable_password_entropy_meter: boolean;
}

/*
* NOTE: This store is written to use MobX, but its imports do not need to be
Expand Down Expand Up @@ -76,7 +111,7 @@ class EnvStoreData {
storage: null,
data: null,
transcription_minutes: null,
translation_chars: null
translation_chars: null,
};
public free_tier_display: FreeTierDisplay = {name: null, feature_list: []};
public enable_custom_password_guidance_text = false;
Expand Down Expand Up @@ -122,9 +157,17 @@ class EnvStore {
constructor() {
makeAutoObservable(this);
this.data = new EnvStoreData();
this.fetchData();
}

actions.auth.getEnvironment.completed.listen(this.onGetEnvCompleted.bind(this));
actions.auth.getEnvironment();
async fetchData() {
try {
const response = await fetchGet<EnvironmentResponse>(ENV_ENDPOINT);
this.onGetEnvCompleted(response);
} catch (err) {
const errorObj = err as FailResponse;
handleApiFail(errorObj);
}
}
Copy link
Contributor

@LMNTL LMNTL Oct 25, 2023

Choose a reason for hiding this comment

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

This catch block is kind of redundant - because fetchGet() (without the notifyAboutError option set to false) already calls handleApiFail(). So if I make the /environment endpoint error, I see two error notifications:

image

Copy link
Member Author

Choose a reason for hiding this comment

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

Right! Pushed a fix :)


/**
Expand Down Expand Up @@ -161,23 +204,33 @@ class EnvStore {
this.data.free_tier_display = response.free_tier_display;

if (response.sector_choices) {
this.data.sector_choices = response.sector_choices.map(this.nestedArrToChoiceObjs);
this.data.sector_choices = response.sector_choices.map(
this.nestedArrToChoiceObjs
);
}
if (response.operational_purpose_choices) {
this.data.operational_purpose_choices = response.operational_purpose_choices.map(this.nestedArrToChoiceObjs);
this.data.operational_purpose_choices =
response.operational_purpose_choices.map(this.nestedArrToChoiceObjs);
}
if (response.country_choices) {
this.data.country_choices = response.country_choices.map(this.nestedArrToChoiceObjs);
this.data.country_choices = response.country_choices.map(
this.nestedArrToChoiceObjs
);
}
if (response.interface_languages) {
this.data.interface_languages = response.interface_languages.map(this.nestedArrToChoiceObjs);
this.data.interface_languages = response.interface_languages.map(
this.nestedArrToChoiceObjs
);
}

this.data.asr_mt_features_enabled = response.asr_mt_features_enabled;

this.data.enable_custom_password_guidance_text = response.enable_custom_password_guidance_text;
this.data.custom_password_localized_help_text = response.custom_password_localized_help_text;
this.data.enable_password_entropy_meter = response.enable_password_entropy_meter;
this.data.enable_custom_password_guidance_text =
response.enable_custom_password_guidance_text;
this.data.custom_password_localized_help_text =
response.custom_password_localized_help_text;
this.data.enable_password_entropy_meter =
response.enable_password_entropy_meter;

this.isReady = true;
}
Expand Down Expand Up @@ -207,4 +260,4 @@ class EnvStore {
* This store keeps all environment data (constants) like languages, countries,
* external urls…
*/
export default new EnvStore;
export default new EnvStore();