BC-5 fix: general tab of preferences form submitting.

This commit is contained in:
a.bouhuolia
2021-09-04 18:49:01 +02:00
parent d6d6fefd1f
commit 11df54d4ed
25 changed files with 251 additions and 131 deletions

View File

@@ -19,7 +19,6 @@ import { Icon, Hint, If } from 'components';
import withUniversalSearchActions from 'containers/UniversalSearch/withUniversalSearchActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withDashboard from 'containers/Dashboard/withDashboard';
import withSettings from 'containers/Settings/withSettings';
import QuickNewDropdown from 'containers/QuickNewDropdown/QuickNewDropdown';
import { compose } from 'utils';
@@ -76,9 +75,6 @@ function DashboardTopbar({
// #withDashboard
sidebarExpended,
// #withSettings
organizationName,
// #withGlobalSearch
openGlobalSearch,
@@ -190,9 +186,6 @@ export default compose(
sidebarExpended,
pageHint,
})),
withSettings(({ organizationSettings }) => ({
organizationName: organizationSettings.name,
})),
withDashboardActions,
withSubscriptions(
({ isSubscriptionActive, isSubscriptionInactive }) => ({

View File

@@ -2,8 +2,8 @@ import React from 'react';
import { Button, Popover, Menu, Position } from '@blueprintjs/core';
import Icon from 'components/Icon';
import { useAuthUser } from 'hooks/state';
import withSettings from 'containers/Settings/withSettings';
import { compose, firstLettersArgs } from 'utils';
import withCurrentOrganization from '../../containers/Organization/withCurrentOrganization';
// Popover modifiers.
const POPOVER_MODIFIERS = {
@@ -14,8 +14,8 @@ const POPOVER_MODIFIERS = {
* Sideabr head.
*/
function SidebarHead({
// #withSettings
organizationName,
// #withCurrentOrganization
organization,
}) {
const user = useAuthUser();
@@ -29,9 +29,9 @@ function SidebarHead({
<Menu className={'menu--dashboard-organization'}>
<div class="org-item">
<div class="org-item__logo">
{firstLettersArgs(...organizationName.split(' '))}{' '}
{firstLettersArgs(...(organization.name || '').split(' '))}{' '}
</div>
<div class="org-item__name">{organizationName}</div>
<div class="org-item__name">{organization.name}</div>
</div>
</Menu>
}
@@ -42,7 +42,7 @@ function SidebarHead({
className="title"
rightIcon={<Icon icon={'caret-down-16'} size={16} />}
>
{organizationName}
{organization.name}
</Button>
</Popover>
<span class="subtitle">{user.full_name}</span>
@@ -61,7 +61,5 @@ function SidebarHead({
}
export default compose(
withSettings(({ organizationSettings }) => ({
organizationName: organizationSettings.name,
})),
withCurrentOrganization(({ organization }) => ({ organization })),
)(SidebarHead);

View File

@@ -2,14 +2,25 @@ import React, { useEffect } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import HomepageContent from './HomepageContent';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withSettings from 'containers/Settings/withSettings';
import withCurrentOrganization from '../Organization/withCurrentOrganization';
import { compose } from 'utils';
function DashboardHomepage({ changePageTitle, name }) {
/**
* Dashboard homepage.
*/
function DashboardHomepage({
// #withDashboardActions
changePageTitle,
// #withCurrentOrganization
organization,
}) {
useEffect(() => {
changePageTitle(name);
}, [name, changePageTitle]);
changePageTitle(organization.name);
}, [organization.name, changePageTitle]);
return (
<DashboardInsider name="homepage">
@@ -20,7 +31,5 @@ function DashboardHomepage({ changePageTitle, name }) {
export default compose(
withDashboardActions,
withSettings(({ organizationSettings }) => ({
name: organizationSettings.name,
})),
withCurrentOrganization(({ organization }) => ({ organization })),
)(DashboardHomepage);

View File

@@ -1,12 +1,16 @@
import { connect } from 'react-redux';
import { getCurrentOrganizationFactory } from '../../store/authentication/authentication.selectors';
export default (mapState) => {
const getCurrentOrganization = getCurrentOrganizationFactory();
const mapStateToProps = (state, props) => {
const mapped = {
organizationTenantId: state.authentication.organizationId,
organizationId: state.authentication.organization,
organization: getCurrentOrganization(state, props),
};
return (mapState) ? mapState(mapped, state, props) : mapped;
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};
};

View File

@@ -5,9 +5,6 @@ const Schema = Yup.object().shape({
name: Yup.string()
.required()
.label(intl.get('organization_name_')),
financial_date_start: Yup.date()
.required()
.label(intl.get('date_start_')),
industry: Yup.string()
.nullable()
.label(intl.get('organization_industry_')),
@@ -23,7 +20,7 @@ const Schema = Yup.object().shape({
language: Yup.string()
.required()
.label(intl.get('language')),
time_zone: Yup.string()
timezone: Yup.string()
.required()
.label(intl.get('time_zone_')),
date_format: Yup.string()

View File

@@ -1,42 +1,35 @@
import { Form } from 'formik';
import React from 'react';
import {
Button,
FormGroup,
InputGroup,
Intent,
Position,
} from '@blueprintjs/core';
import { Button, FormGroup, InputGroup, Intent } from '@blueprintjs/core';
import classNames from 'classnames';
import { TimezonePicker } from '@blueprintjs/timezone';
import { ErrorMessage, FastField } from 'formik';
import { DateInput } from '@blueprintjs/datetime';
import { useHistory } from 'react-router-dom';
import { FormattedMessage as T } from 'components';
import { ListSelect, FieldRequiredHint } from 'components';
import {
inputIntent,
momentFormatter,
tansformDateValue,
handleDateChange,
} from 'utils';
import { inputIntent } from 'utils';
import { CLASSES } from 'common/classes';
import { getCountries } from 'common/countries';
import { getCurrencies } from 'common/currencies';
import { getAllCurrenciesOptions } from 'common/currencies';
import { getFiscalYear } from 'common/fiscalYearOptions';
import { getLanguages } from 'common/languagesOptions';
import { getDateFormats } from 'common/dateFormatsOptions';
import { useGeneralFormContext } from './GeneralFormProvider';
export default function PreferencesGeneralForm({}) {
/**
* Preferences general form.
*/
export default function PreferencesGeneralForm({ isSubmitting }) {
const history = useHistory();
const FiscalYear = getFiscalYear();
const Countries = getCountries();
const Languages = getLanguages();
const Currencies = getCurrencies();
const DataFormats = getDateFormats();
const Currencies = getAllCurrenciesOptions();
const { dateFormats } = useGeneralFormContext();
// Handle close click.
const handleCloseClick = () => {
history.go(-1);
};
@@ -59,29 +52,7 @@ export default function PreferencesGeneralForm({}) {
)}
</FastField>
{/* ---------- Financial starting date ---------- */}
<FastField name={'financial_date_start'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'financial_starting_date'} />}
labelInfo={<FieldRequiredHint />}
inline={true}
intent={inputIntent({ error, touched })}
className={classNames('form-group--select-list', CLASSES.FILL)}
helperText={<T id={'for_reporting_you_can_specify_any_month'} />}
>
<DateInput
{...momentFormatter('MMMM Do YYYY')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('financial_date_start', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
/>
</FormGroup>
)}
</FastField>
{/* ---------- Industry ---------- */}
<FastField name={'industry'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
@@ -147,10 +118,10 @@ export default function PreferencesGeneralForm({}) {
form.setFieldValue('base_currency', currency.code);
}}
selectedItem={value}
selectedItemProp={'code'}
selectedItemProp={'key'}
defaultText={<T id={'select_base_currency'} />}
textProp={'name'}
labelProp={'code'}
labelProp={'key'}
popoverProps={{ minimal: true }}
/>
</FormGroup>
@@ -165,8 +136,8 @@ export default function PreferencesGeneralForm({}) {
labelInfo={<FieldRequiredHint />}
className={classNames('form-group--fiscal-year', CLASSES.FILL)}
inline={true}
helperText={<ErrorMessage name="fiscal_year" />}
intent={inputIntent({ error, touched })}
helperText={<T id={'for_reporting_you_can_specify_any_month'} />}
>
<ListSelect
items={FiscalYear}
@@ -174,7 +145,7 @@ export default function PreferencesGeneralForm({}) {
form.setFieldValue('fiscal_year', value)
}
selectedItem={value}
selectedItemProp={'value'}
selectedItemProp={'key'}
defaultText={<T id={'select_fiscal_year'} />}
textProp={'name'}
popoverProps={{ minimal: true }}
@@ -210,7 +181,7 @@ export default function PreferencesGeneralForm({}) {
</FastField>
{/* ---------- Time zone ---------- */}
<FastField name={'time_zone'}>
<FastField name={'timezone'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'time_zone'} />}
@@ -222,12 +193,12 @@ export default function PreferencesGeneralForm({}) {
CLASSES.FILL,
)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="time_zone" />}
helperText={<ErrorMessage name="timezone" />}
>
<TimezonePicker
value={value}
onChange={(timezone) => {
form.setFieldValue('time_zone', timezone);
form.setFieldValue('timezone', timezone);
}}
valueDisplayFormat="composite"
placeholder={<T id={'select_time_zone'} />}
@@ -248,15 +219,14 @@ export default function PreferencesGeneralForm({}) {
helperText={<ErrorMessage name="date_format" />}
>
<ListSelect
items={DataFormats}
items={dateFormats}
onItemSelect={(dateFormat) => {
form.setFieldValue('date_format', dateFormat.value);
form.setFieldValue('date_format', dateFormat.key);
}}
selectedItem={value}
selectedItemProp={'value'}
selectedItemProp={'key'}
defaultText={<T id={'select_date_format'} />}
textProp={'name'}
labelProp={'label'}
textProp={'label'}
popoverProps={{ minimal: true }}
/>
</FormGroup>
@@ -264,7 +234,7 @@ export default function PreferencesGeneralForm({}) {
</FastField>
<div className={'card__footer'}>
<Button intent={Intent.PRIMARY} type="submit">
<Button loading={isSubmitting} intent={Intent.PRIMARY} type="submit">
<T id={'save'} />
</Button>
<Button onClick={handleCloseClick}>

View File

@@ -1,53 +1,53 @@
import React, { useEffect } from 'react';
import { Formik } from 'formik';
import { mapKeys, snakeCase } from 'lodash';
import { Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import 'style/pages/Preferences/GeneralForm.scss';
import { AppToaster } from 'components';
import GeneralForm from './GeneralForm';
import { PreferencesGeneralSchema } from './General.schema';
import { useGeneralFormContext } from './GeneralFormProvider';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withSettings from 'containers/Settings/withSettings';
import { compose, optionsMapToArray } from 'utils';
import { compose } from 'utils';
import { transformToForm } from '../../../utils';
import 'style/pages/Preferences/GeneralForm.scss';
const defaultValues = {
name: '',
industry: '',
location: '',
base_currency: '',
language: '',
fiscal_year: '',
date_format: '',
timezone: '',
};
/**
* Preferences - General form Page.
*/
function GeneralFormPage({
// #withSettings
organizationSettings,
//# withDashboardActions
// #withDashboardActions
changePreferencesPageTitle,
}) {
const { saveSettingMutate } = useGeneralFormContext();
const { updateOrganization, organization } = useGeneralFormContext();
useEffect(() => {
changePreferencesPageTitle(intl.get('general'));
}, [changePreferencesPageTitle]);
function transformGeneralSettings(data) {
return mapKeys(data, (value, key) => snakeCase(key));
}
// Initial values.
const initialValues = {
...transformGeneralSettings(organizationSettings),
...transformToForm(organization.metadata, defaultValues),
};
const handleFormSubmit = (values, { setSubmitting, resetForm }) => {
const options = optionsMapToArray(values).map((option) => {
return { key: option.key, ...option, group: 'organization' };
});
// Handle request success.
const onSuccess = (response) => {
AppToaster.show({
message: 'The general preferences has been saved.',
message: intl.get('preferences.general.success_message'),
intent: Intent.SUCCESS,
});
setSubmitting(false);
@@ -57,7 +57,9 @@ function GeneralFormPage({
const onError = (errors) => {
setSubmitting(false);
};
saveSettingMutate({ options }).then(onSuccess).catch(onError);
updateOrganization({ ...values })
.then(onSuccess)
.catch(onError);
};
return (
@@ -70,7 +72,4 @@ function GeneralFormPage({
);
}
export default compose(
withSettings(({ organizationSettings }) => ({ organizationSettings })),
withDashboardActions,
)(GeneralFormPage);
export default compose(withDashboardActions)(GeneralFormPage);

View File

@@ -1,7 +1,11 @@
import React, { createContext } from 'react';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { useSaveSettings, useSettings } from 'hooks/query';
import {
useCurrentOrganization,
useUpdateOrganization,
useDateFormats,
} from 'hooks/query';
import PreferencesPageLoader from '../PreferencesPageLoader';
const GeneralFormContext = createContext();
@@ -10,20 +14,25 @@ const GeneralFormContext = createContext();
* General form provider.
*/
function GeneralFormProvider({ ...props }) {
// Fetches Organization Settings.
const { isLoading: isSettingsLoading } = useSettings();
// Fetches current organization information.
const { isLoading: isOrganizationLoading, data: organization } =
useCurrentOrganization();
// Save Organization Settings.
const { mutateAsync: saveSettingMutate } = useSaveSettings();
const { data: dateFormats, isLoading: isDateFormatsLoading } =
useDateFormats();
// Mutate organization information.
const { mutateAsync: updateOrganization } = useUpdateOrganization();
// Provider state.
const provider = {
isSettingsLoading,
saveSettingMutate,
isOrganizationLoading,
isDateFormatsLoading,
updateOrganization,
organization,
dateFormats,
};
const loading = isSettingsLoading;
return (
<div
className={classNames(
@@ -32,7 +41,7 @@ function GeneralFormProvider({ ...props }) {
)}
>
<div className={classNames(CLASSES.CARD)}>
{loading ? (
{isOrganizationLoading || isDateFormatsLoading ? (
<PreferencesPageLoader />
) : (
<GeneralFormContext.Provider value={provider} {...props} />

View File

@@ -4,7 +4,7 @@ import intl from 'react-intl-universal';
// Retrieve the setup organization form validation.
export const getSetupOrganizationValidation = () =>
Yup.object().shape({
organizationName: Yup.string()
name: Yup.string()
.required()
.label(intl.get('organization_name_')),
location: Yup.string()

View File

@@ -36,13 +36,13 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
</h3>
{/* ---------- Organization name ---------- */}
<FastField name={'organizationName'}>
<FastField name={'name'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'legal_organization_name'} />}
className={'form-group--name'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'organizationName'} />}
helperText={<ErrorMessage name={'name'} />}
>
<InputGroup {...field} intent={inputIntent({ error, touched })} />
</FormGroup>

View File

@@ -14,7 +14,7 @@ import { getSetupOrganizationValidation } from './SetupOrganization.schema';
// Initial values.
const defaultValues = {
organizationName: '',
name: '',
location: 'libya',
baseCurrency: '',
language: 'en',

View File

@@ -27,3 +27,4 @@ export * from './landedCost';
export * from './UniversalSearch/UniversalSearch';
export * from './GenericResource';
export * from './jobs';
export * from './misc';

View File

@@ -0,0 +1,16 @@
import { useRequestQuery } from '../useQueryRequest';
/**
* Retrieve the job metadata.
*/
export function useDateFormats(props = {}) {
return useRequestQuery(
['DATE_FORMATS'],
{ method: 'get', url: `/date_formats` },
{
select: (res) => res.data.data,
defaultData: [],
...props,
},
);
}

View File

@@ -72,3 +72,22 @@ export function useOrganizationSetup() {
},
);
}
/**
* Saves the settings.
*/
export function useUpdateOrganization(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation(
(information) => apiRequest.put('organization', information),
{
onSuccess: () => {
queryClient.invalidateQueries(t.ORGANIZATION_CURRENT);
queryClient.invalidateQueries(t.ORGANIZATIONS);
},
...props,
},
);
}

View File

@@ -1249,5 +1249,6 @@
"billing.suspend_message.description": "Your account has been suspended due to the expiration of the subscription period. Please renew the subscription to activate the account.",
"dashboard.subscription_msg.period_over": "Subscription period is over",
"inventory_adjustment.details_drawer.title": "Inventory adjustment details",
"setup.organization.location": "Location"
"setup.organization.location": "Location",
"preferences.general.success_message": "The general preferences has been saved."
}

View File

@@ -0,0 +1,24 @@
import { defaultTo } from 'lodash';
import { createSelector } from '@reduxjs/toolkit';
const getCurrentOrganizationId = (state) => state.authentication.organization;
const getCurrentTenantId = (state) => state.authentication.organizationId;
const getOrganizationsMap = (state) => state.organizations.data;
// Retrieve organization tenant id.
export const getOrganizationTenantIdFactory = () =>
createSelector(getCurrentTenantId, (tenantId) => tenantId);
// Retrieve organization id.
export const getOrganizationIdFactory = () =>
createSelector(getCurrentOrganizationId, (tenantId) => tenantId);
// Retrieve current organization meta object.
export const getCurrentOrganizationFactory = () =>
createSelector(
getCurrentTenantId,
getOrganizationsMap,
(tenantId, organizationsMap) => {
return defaultTo(organizationsMap[tenantId], {});
},
);

View File

@@ -1,4 +1,5 @@
import { createReducer } from '@reduxjs/toolkit';
import { omit } from 'lodash';
import t from 'store/types';
const initialState = {
@@ -16,7 +17,8 @@ const reducer = createReducer(initialState, {
organizations.forEach((organization) => {
_data[organization.id] = {
...state.data[organization.id],
...organization,
...organization.metadata,
...omit(organization, ['metadata']),
};
_dataByOrganizationId[organization.organization_id] = organization.id;
});

View File

@@ -0,0 +1,41 @@
import { Inject, Service } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express';
import BaseController from 'api/controllers/BaseController';
import MiscService from 'services/Miscellaneous/MiscService';
import DateFormatsService from 'services/Miscellaneous/DateFormats';
@Service()
export default class MiscController extends BaseController {
@Inject()
dateFormatsService: DateFormatsService;
/**
* Express router.
*/
router() {
const router = Router();
router.get(
'/date_formats',
this.validationResult,
this.asyncMiddleware(this.dateFormats.bind(this))
);
return router;
}
/**
* Retrieve date formats options.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
dateFormats(req: Request, res: Response, next: NextFunction) {
try {
const dateFormats = this.dateFormatsService.getDateFormats();
return res.status(200).send({ data: dateFormats });
} catch (error) {
next(error);
}
}
}

View File

@@ -13,8 +13,8 @@ import {
ACCEPTED_CURRENCIES,
MONTHS,
ACCEPTED_LOCALES,
DATE_FORMATS,
} from 'services/Organization/constants';
import { DATE_FORMATS } from 'services/Miscellaneous/DateFormats/constants';
import { ServiceError } from 'exceptions';
import BaseController from 'api/controllers/BaseController';
@@ -64,7 +64,7 @@ export default class OrganizationController extends BaseController {
*/
private get buildValidationSchema(): ValidationChain[] {
return [
check('organization_name').exists().trim(),
check('name').exists().trim(),
check('base_currency').exists().isIn(ACCEPTED_CURRENCIES),
check('timezone').exists().isIn(moment.tz.names()),
check('fiscal_year').exists().isIn(MONTHS),

View File

@@ -42,6 +42,7 @@ import Licenses from 'api/controllers/Subscription/Licenses';
import InventoryAdjustments from 'api/controllers/Inventory/InventoryAdjustments';
import asyncRenderMiddleware from './middleware/AsyncRenderMiddleware';
import Jobs from './controllers/Jobs';
import Miscellaneous from 'api/controllers/Miscellaneous';
export default () => {
const app = Router();
@@ -95,6 +96,8 @@ export default () => {
dashboard.use('/media', Container.get(Media).router());
dashboard.use('/inventory_adjustments', Container.get(InventoryAdjustments).router());
dashboard.use('/', Container.get(Miscellaneous).router());
app.use('/', dashboard);
return app;

View File

@@ -0,0 +1,11 @@
export const DATE_FORMATS = [
'MM/DD/YY',
'DD/MM/YY',
'YY/MM/DD',
'MM/DD/yyyy',
'DD/MM/yyyy',
'yyyy/MM/DD',
'DD MMM YYYY',
'DD MMMM YYYY',
'MMMM DD, YYYY',
];

View File

@@ -0,0 +1,15 @@
import moment from 'moment-timezone';
import { Service } from 'typedi';
import { DATE_FORMATS } from './constants';
@Service()
export default class DateFormatsService {
getDateFormats() {
return DATE_FORMATS.map((dateFormat) => {
return {
label: `${moment().format(dateFormat)} [${dateFormat}]`,
key: dateFormat,
};
});
}
}

View File

@@ -0,0 +1,8 @@
import { Service } from 'typedi';
@Service()
export default class MiscService {
getDateFormats() {
return [];
}
}

View File

@@ -1,4 +1,5 @@
import { Service, Inject } from 'typedi';
import { ObjectId } from 'mongodb';
import { ServiceError } from 'exceptions';
import {
IOrganizationBuildDTO,
@@ -12,7 +13,6 @@ import {
import events from 'subscribers/events';
import TenantsManager from 'services/Tenancy/TenantsManager';
import { Tenant } from 'system/models';
import { ObjectId } from 'mongodb';
const ERRORS = {
TENANT_NOT_FOUND: 'tenant_not_found',
@@ -137,8 +137,8 @@ export default class OrganizationService {
/**
* Updates organization information.
* @param {ITenant} tenantId
* @param {IOrganizationUpdateDTO} organizationDTO
* @param {ITenant} tenantId
* @param {IOrganizationUpdateDTO} organizationDTO
*/
public async updateOrganization(
tenantId: number,

View File

@@ -3,8 +3,9 @@ exports.up = function (knex) {
table.bigIncrements();
table.integer('tenant_id').unsigned();
table.string('organization_name');
table.string('name');
table.string('industry');
table.string('location');
table.string('base_currency');
table.string('language');
@@ -13,7 +14,6 @@ exports.up = function (knex) {
table.string('date_format');
table.string('fiscal_year');
table.string('financial_start_date');
});
};