Commit 56992596 authored by aleclofabbro's avatar aleclofabbro

refactored localization, removed Redux completely, fixed settings storybook

parent eda95916
import React, { FC } from 'react';
import React, { FC, useCallback, useMemo } from 'react';
import Preferences, { EditPreferences } from 'ui/pages/settings/preferences';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import { DOMAIN_REGEX } from 'mn-constants';
import { useMe } from 'fe/session/useMe';
import { LocaleContext } from 'context/global/localizationCtx';
const validationSchema = Yup.object<EditPreferences>({
moodleWebsite: Yup.string().matches(DOMAIN_REGEX)
});
export const PreferencesSettingsSection: FC = () => {
const { available, current, set } = React.useContext(LocaleContext);
const { me, updateProfile } = useMe();
const profile = me?.user;
const formik = useFormik<EditPreferences>({
......@@ -28,5 +31,33 @@ export const PreferencesSettingsSection: FC = () => {
});
}
});
return <Preferences formik={formik} />;
const localesOptions = useMemo(
() =>
available.map(locale => ({
value: locale.code,
label: locale.desc
})),
[available]
);
const currentOption = useMemo(
() =>
localesOptions.find(option => option.value === current.code) ||
localesOptions[0],
[localesOptions]
);
const setLocale = useCallback((code: string) => {
const locale = available.find(locale => locale.code === code);
locale && set(locale);
}, []);
return (
<Preferences
formik={formik}
current={currentOption}
locales={localesOptions}
setLocale={setLocale}
/>
);
};
......@@ -41,16 +41,11 @@ export type OperationName = QueryName | MutationName;
interface Cfg {
localKVStore: KVStore;
appLinks: ApolloLink[];
dispatch(payload: any);
}
const AUTH_TOKEN_KEY = 'AUTH_TOKEN';
export default async function initialise({
localKVStore,
appLinks,
dispatch
}: Cfg) {
export default async function initialise({ localKVStore, appLinks }: Cfg) {
let authToken = localKVStore.get(AUTH_TOKEN_KEY);
const fragmentMatcher = new IntrospectionFragmentMatcher({
introspectionQueryResultData
......
import React, { useContext } from 'react';
import { createContext } from 'react';
import { StoreContext } from './storeCtx';
export interface ActionContextT {
dispatch: (_: any) => unknown;
}
export const ActionContext = createContext<ActionContextT>(
{} as ActionContextT
);
export const ProvideActionCtx: React.FC = ({ children }) => {
const { dispatch } = useContext(StoreContext);
return (
<ActionContext.Provider value={{ dispatch }}>
{children}
</ActionContext.Provider>
);
};
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import { ProvideActionCtx } from './actionCtx';
import { ProvideAlgoliaContext } from './algolia';
import { ProvideLocalizationCtx } from './localizationCtx';
import { ProvideStateCtx } from './stateCtx';
import { ProvideStoreCtx, StoreContextT } from './storeCtx';
interface Props {
children: React.ReactNode;
store: StoreContextT;
}
export const ProvideContexts: React.FC<Props> = ({ children, store }) => {
export const ProvideContexts: React.FC<Props> = ({ children }) => {
return (
<ProvideStoreCtx store={store}>
<ProvideStateCtx>
<ProvideActionCtx>
<ProvideLocalizationCtx>
<BrowserRouter>
<ProvideAlgoliaContext>{children}</ProvideAlgoliaContext>
</BrowserRouter>
</ProvideLocalizationCtx>
</ProvideActionCtx>
</ProvideStateCtx>
</ProvideStoreCtx>
<ProvideLocalizationCtx>
<BrowserRouter>
<ProvideAlgoliaContext>{children}</ProvideAlgoliaContext>
</BrowserRouter>
</ProvideLocalizationCtx>
);
};
import { setupI18n, I18n, Catalog, Catalogs } from '@lingui/core';
import { Settings } from 'luxon';
import { Catalog, Catalogs, I18n, setupI18n } from '@lingui/core';
import { I18nProvider } from '@lingui/react';
import React, {
createContext,
useContext,
useMemo,
useEffect,
useState
useMemo,
useState,
useCallback
} from 'react';
import { StateContext } from './stateCtx';
import { IS_DEV, locales, LocaleKey } from '../../mn-constants';
import { IS_DEV, LocaleDef, locales as available } from '../../mn-constants';
import {
createLocalSessionKVStorage,
LOCAL
} from 'util/keyvaluestore/localSessionStorage';
const LOCALIZATION_STORE = 'LOCALIZATION';
const LOCALE_KEY = '#locale';
const kvstore = createLocalSessionKVStorage(LOCAL)(LOCALIZATION_STORE);
export type LocaleContextT = {
locale: LocaleKey;
current: LocaleDef;
i18n: I18n;
RTL: boolean;
available: LocaleDef[];
set(locale: LocaleDef): void;
};
const getStoredLocaleCode = (): string | null => kvstore.get(LOCALE_KEY);
const setStoredLocaleCode = (localeCode: string): void =>
kvstore.set(LOCALE_KEY, localeCode);
export const i18n = setupI18n({ locales: locales });
const savedLangCode = getStoredLocaleCode();
const defaultLocale =
(savedLangCode && available.find(locale => locale.code === savedLangCode)) ||
available[0];
export const i18n = setupI18n({
locales: available.map(locale => locale.code)
});
export const LocaleContext = createContext<LocaleContextT>({
locale: locales[0],
current: defaultLocale,
i18n,
RTL: false
available,
set: () => void 0
});
export const ProvideLocalizationCtx: React.FC = ({ children }) => {
const {
localization: { locale }
} = useContext(StateContext);
const [current, setCurrent] = useState(defaultLocale);
const [catalogs, setCatalogs] = useState<Catalogs>({});
const RTL = isLocaleRTL(locale);
useEffect(() => {
setHTMLDirection(RTL);
if (!locales.includes(locale) || catalogs[locale]) {
setHTMLDirection(current.rtl);
if (catalogs[current.code]) {
return;
}
loadCatalog(locale)
.then(cat => setCatalogs({ ...catalogs, [locale]: cat }))
.catch(err => console.error(`Error loading Locale: ${locale}`, err));
}, [locale, RTL]);
loadCatalog(current.code)
.then(cat => setCatalogs({ ...catalogs, [current.code]: cat }))
.catch(err =>
console.error(`Error loading Locale: ${current.code}`, err)
);
}, [current]);
const set = useCallback(
(locale: LocaleDef) => {
setCurrent(locale);
const { code } = locale;
Settings.defaultLocale = code.split('_')[0];
setStoredLocaleCode(code);
},
[setCurrent]
);
const localeContextValue = useMemo<LocaleContextT>(
() => ({
locale,
current,
available,
i18n,
RTL
set
}),
[locale, i18n]
[current, i18n, set]
);
return (
<I18nProvider i18n={i18n} language={locale} catalogs={catalogs}>
<I18nProvider i18n={i18n} language={current.code} catalogs={catalogs}>
<LocaleContext.Provider value={localeContextValue}>
{children}
</LocaleContext.Provider>
......@@ -57,24 +84,20 @@ export const ProvideLocalizationCtx: React.FC = ({ children }) => {
);
};
const loadCatalog = async (locale: LocaleKey): Promise<Catalog> => {
const loadCatalog = async (localeCode: string): Promise<Catalog> => {
if (IS_DEV) {
return import(
/* webpackMode: "lazy", webpackChunkName: "i18n-[index]" */
`@lingui/loader!../../locales/${locale}/messages.po`
`@lingui/loader!../../locales/${localeCode}/messages.po`
);
} else {
return import(
/* webpackMode: "lazy", webpackChunkName: "i18n-[index]" */
`../../locales/${locale}/messages.js`
`../../locales/${localeCode}/messages.js`
);
}
};
const isLocaleRTL = (locale: LocaleKey) => {
return locale === 'ar_SA';
};
const setHTMLDirection = (RTL: boolean) => {
const htmlEl = document.querySelector('html');
if (htmlEl) {
......
import React, { useContext, useState, useEffect } from 'react';
import { createContext } from 'react';
import { StoreContext } from './storeCtx';
import { State } from '../../redux/store';
export type StateContextT = State;
export const StateContext = createContext<StateContextT>({} as StateContextT);
export const ProvideStateCtx: React.FC = ({ children }) => {
const store = useContext(StoreContext);
const [state, setState] = useState(store.getState());
useEffect(() => store.subscribe(() => setState(store.getState())), [store]);
return (
<StateContext.Provider value={state}>{children}</StateContext.Provider>
);
};
import React from 'react';
import store from '../../redux/store';
import { createContext } from 'react';
export type StoreContextT = ReturnType<typeof store>;
export const StoreContext = createContext<StoreContextT>({} as StoreContextT);
interface Props {
store: StoreContextT;
}
export const ProvideStoreCtx: React.FC<Props> = ({ children, store }) => {
return (
<StoreContext.Provider value={store}>{children}</StoreContext.Provider>
);
};
......@@ -12,7 +12,6 @@ import App from './containers/App/App';
import { ProvideContexts } from './context/global';
import * as K from './mn-constants';
import { colors, typography } from './mn-constants';
import createStore from './redux/store';
import registerServiceWorker from './registerServiceWorker';
import { createLocalSessionKVStorage } from './util/keyvaluestore/localSessionStorage';
......@@ -87,17 +86,15 @@ async function run() {
input:focus:-ms-input-placeholder, textarea:focus:-ms-input-placeholder { color:transparent; } /* IE 10+ */
`;
const createLocalKVStore = createLocalSessionKVStorage('local');
const store = createStore({ createLocalKVStore });
const apolloClient = await getApolloClient({
localKVStore: createLocalKVStore('APOLLO#'),
appLinks: [MngErrorLink],
dispatch: store.dispatch
appLinks: [MngErrorLink]
});
const ApolloApp = () => (
<ApolloProvider client={apolloClient.client}>
<ProvideContexts store={store}>
<ProvideContexts>
<Global />
<ToastContainer
hideProgressBar
......
......@@ -61,17 +61,17 @@ export const related_urls = {
export const IS_DEV = NODE_ENV === 'development';
export const languages = {
en_GB: 'English, British',
en_US: 'English, USA',
es_MX: 'Español, Méjico',
es_ES: 'Español, España',
fr_FR: 'Français, France',
eu: 'Euskara',
ar_SA: 'العربية, المملكة العربية السعودية'
};
export type LocaleKey = keyof typeof languages;
export const locales = Object.keys(languages) as LocaleKey[];
export type LocaleDef = { code: string; desc: string; rtl: boolean };
export const locales: LocaleDef[] = [
{ code: 'en_GB', desc: 'English, British', rtl: false },
{ code: 'en_US', desc: 'English, USA', rtl: false },
{ code: 'es_MX', desc: 'Español, Méjico', rtl: false },
{ code: 'es_ES', desc: 'Español, España', rtl: false },
{ code: 'fr_FR', desc: 'Français, France', rtl: false },
{ code: 'eu', desc: 'Euskara', rtl: false },
{ code: 'ar_SA', desc: 'العربية, المملكة العربية السعودية', rtl: true }
];
const mothershipAppId = process.env.REACT_APP_MOTHERSHIP_API_ID;
const mothershipApiKey = process.env.REACT_APP_MOTHERSHIP_API_KEY;
......
import { actionCtx } from '../../util/redux/Actions';
import { LocaleKey } from '../../mn-constants';
export const setLang = actionCtx<'localization.setLang', LocaleKey>(
'localization.setLang'
);
export * from './action';
export * from './reducer';
export * from './mw';
export * from './types';
import { Settings } from 'luxon';
import { AnyAction, Middleware, Reducer } from 'redux';
import * as Localization from '.';
import { locales, LocaleKey } from '../../mn-constants';
import { KVStore } from '../../util/keyvaluestore/types';
const LOCALE_KEY = 'locale';
interface LocalizationSrv {
mw: Middleware;
reducer: Reducer<Localization.State, AnyAction>;
}
const defaultLang = locales[0];
export const createLocalizationMW = (kvstore: KVStore): LocalizationSrv => {
const getStoredLang = (): LocaleKey | null => kvstore.get(LOCALE_KEY);
// const delStoredLang = (): Localization.Lang | null => kvstore.del(LANG_KEY);
const setStoredLang = (locale: LocaleKey): void =>
kvstore.set(LOCALE_KEY, locale);
const mw: Middleware = store => next => {
return action => {
if (Localization.setLang.is(action)) {
Settings.defaultLocale = action.payload.split('_')[0];
setStoredLang(action.payload);
}
next(action);
};
};
const initialLocale = getStoredLang() || defaultLang;
Settings.defaultLocale = initialLocale.split('_')[0];
Settings.defaultZoneName = 'UTC';
const initialState: Localization.State = {
locale: initialLocale
};
const reducer = Localization.makeReducer(initialState);
return {
mw,
reducer
};
};
import * as Loc from '.';
import { Reducer } from 'redux';
export const makeReducer = (initialState: Loc.State): Reducer<Loc.State> => (
old = initialState,
action
) => {
if (Loc.setLang.is(action)) {
return {
...old,
locale: action.payload
};
}
return old;
};
import { LocaleKey } from '../../mn-constants';
export interface State {
locale: LocaleKey;
}
import {
applyMiddleware,
combineReducers,
compose,
createStore,
Store
} from 'redux';
import { CreateKVStore } from '../util/keyvaluestore/types';
import { createLocalizationMW } from './localization';
export type State = ReturnType<typeof createAppStore> extends Store<infer S>
? S
: never;
interface Cfg {
createLocalKVStore: CreateKVStore;
}
export const createAppStore = ({ createLocalKVStore }: Cfg) => {
const composeEnhancers =
(window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose();
// const __DEV__ = (window as any).__REDUX_DEVTOOLS_EXTENSION__ && (window as any).__REDUX_DEVTOOLS_EXTENSION__()
const Localization = createLocalizationMW(
createLocalKVStore('LOCALIZATION#')
);
const enhancer = composeEnhancers(applyMiddleware(Localization.mw));
const reducer = combineReducers({
localization: Localization.reducer
});
const store = createStore(reducer, enhancer);
return store;
};
export default createAppStore;
......@@ -49,7 +49,17 @@ export const getEditProfileProps = (): EditProfileProps => {
basePath: '/',
displayUsername: '@estrella@home.moodle.net',
isAdmin: false,
Preferences: <Preferences formik={preferencesFormik} />,
Preferences: (
<Preferences
formik={preferencesFormik}
current={{ label: 'English', value: 'en_GB' }}
locales={[
{ label: 'English', value: 'en_GB' },
{ label: 'Espanol', value: 'es_ES' }
]}
setLocale={action('setLocale')}
/>
),
Flags: <div>Flags section </div>, //FIXME
Instance: <div>Instance section </div>, //FIXME
Invites: <div>Invites section </div>, //FIXME,
......@@ -410,7 +420,17 @@ export const getEditProfilePropsAdmin = (): EditProfileProps => {
formik,
basePath: '/',
displayUsername: '@ammaarah@home.moodle.net',
Preferences: <Preferences formik={preferencesFormik} />,
Preferences: (
<Preferences
formik={preferencesFormik}
current={{ label: 'English', value: 'en_GB' }}
locales={[
{ label: 'English', value: 'en_GB' },
{ label: 'Espanol', value: 'es_ES' }
]}
setLocale={action('setLocale')}
/>
),
Invites: (
<Emails
emailsList={[
......
......@@ -5,9 +5,9 @@ import {Box} from 'rebass'
import {getEditProfilePropsAdmin} from 'ui/mock/settings'
import {MainHeader} from 'ui/modules/MainHeader'
import {getMainHeaderProps} from 'ui/mock/mainHeader'
import {ComponentBag} from 'ui/lib/componentBag'
import {WithoutSidebar} from 'ui/templates/withoutSidebar'
const Head = () => <MainHeader {...getMainHeaderProps() } />
const Head = ComponentBag(MainHeader,getMainHeaderProps())
<Meta title="Pages|Settings - Admin" component={Settings}/>
......
import * as React from 'react';
import Button from 'ui/elements/Button';
import { ContainerForm, Row, Actions } from 'ui/modules/Modal';
import { Trans } from '@lingui/macro';
import { Input, Label } from '@rebass/forms';
import * as React from 'react';
import Select from 'react-select';
import { Box, Text } from 'rebass/styled-components';
import { FormikHook } from 'ui/@types/types';
import Button from 'ui/elements/Button';
import { Actions, ContainerForm, Row } from 'ui/modules/Modal';
// import { ArrowLeft, ArrowRight } from 'react-feather';
// import media from 'styled-media-query';
import styled from '../../themes/styled';
import { LocaleContext } from '../../../context/global/localizationCtx';
import Select from 'react-select';
import { ActionContext } from '../../../context/global/actionCtx';
import { setLang } from '../../../redux/localization';
import { languages, locales } from '../../../mn-constants';
import { FormikHook } from 'ui/@types/types';
import { Label, Input } from '@rebass/forms';
// const Header = styled(Flex)`
// border-bottom: ${props => props.theme.colors.border};
......@@ -36,86 +32,72 @@ export interface EditPreferences {
moodleWebsite: string;
}
export interface Props {
export interface Props extends LanguageSelectProps {
formik: FormikHook<EditPreferences>;
}
type LanguageSelectProps = {
fullWidth?: boolean;
} & React.SelectHTMLAttributes<object>;
const options = locales.map(loc => ({
value: loc,
label: languages[loc]
}));
current: { value: string; label: string };
locales: { value: string; label: string }[];
setLocale(code: string): unknown;
};
export const LanguageSelect: React.FC<LanguageSelectProps> = props => {
const { locale } = React.useContext(LocaleContext);
const { dispatch } = React.useContext(ActionContext);
return (
<Select
options={options}
defaultValue={options.find(_ => _.value === locale)}
onChange={selectedKey => {
const selection =
!!selectedKey && 'length' in selectedKey
? selectedKey[0]
: selectedKey;
options={props.locales as any}
defaultValue={props.current}
onChange={selectedCode => {
const selection = Array.isArray(selectedCode)
? selectedCode[0]
: selectedCode;
if (!selection) {
return;
}
dispatch(setLang.create(selection.value));
props.setLocale(selection.value);
}}
/>
);
};
const Preferences: React.FC<Props> = props => (
<LocaleContext.Consumer>
{value => (
<Box>
<Row>
<ContainerForm>
<label>
<Trans>Select language</Trans>
</label>
<LanguageSelect />
<Box width={1 / 2} mt={2}>
<Label htmlFor="moodleWebsite">Moodle LMS site location</Label>
<Input
id="moodleWebsite"
disabled={props.formik.isSubmitting}
value={props.formik.values.moodleWebsite}
onChange={props.formik.handleChange}
name="moodleWebsite"
placeholder={'Type your Moodle LMS instance'}
/>
</Box>
<Actions sx={{ height: 'inherit !important' }}>
<Button
variant="primary"
isSubmitting={props.formik.isSubmitting}
isDisabled={props.formik.isSubmitting}
type="submit"
style={{ marginLeft: '10px' }}
onClick={props.formik.submitForm}
>
<Trans>Save</Trans>
</Button>
</Actions>
</ContainerForm>
</Row>
<TransifexLink variant="text" my={3} mt={2}>
<a
href="https://www.transifex.com/moodlenet/moodlenet/"
target="_blank"
<Box>
<Row>
<ContainerForm>
<label>
<Trans>Select language</Trans>
</label>
<LanguageSelect {...props} />
<Box width={1 / 2} mt={2}>
<Label htmlFor="moodleWebsite">Moodle LMS site location</Label>
<Input
id="moodleWebsite"
disabled={props.formik.isSubmitting}
value={props.formik.values.moodleWebsite}
onChange={props.formik.handleChange}
name="moodleWebsite"
placeholder={'Type your Moodle LMS instance'}
/>
</Box>
<Actions sx={{ height: 'inherit !important' }}>
<Button
variant="primary"
isSubmitting={props.formik.isSubmitting}
isDisabled={props.formik.isSubmitting}
type="submit"
style={{ marginLeft: '10px' }}
onClick={props.formik.submitForm}
>
<Trans>Want to contibute to MoodleNet translation?</Trans>
</a>
</TransifexLink>
</Box>
)}
</LocaleContext.Consumer>