Merge pull request #858 from bigcapitalhq/migrate-from-cra-to-vite

feat: migrate from CRA to Vite for speed
This commit is contained in:
Ahmed Bouhuolia
2025-11-25 21:36:28 +02:00
committed by GitHub
28 changed files with 3154 additions and 6709 deletions

View File

@@ -24,4 +24,4 @@ RUN pnpm run build:webapp
FROM nginx
COPY ./packages/webapp/nginx/sites/default.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/packages/webapp/build /usr/share/nginx/html
COPY --from=build /app/packages/webapp/dist /usr/share/nginx/html

View File

@@ -1,17 +0,0 @@
const path = require('path');
module.exports = {
webpack: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
configure: {
resolve: {
fallback: { path: require.resolve('path-browserify') },
},
},
},
devServer: {
allowedHosts: process.env.GITPOD_HOST ? 'all' : 'auto'
},
};

View File

@@ -0,0 +1,28 @@
<!doctype html>
<html dir="ltr" lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicons/favicon-32.ico" sizes="32x32" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Bigcapital Financial Managment Software"
/>
<link rel="manifest" href="/manifest.json" />
<title>Bigcapital</title>
</head>
<body class="bp4-dark">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<div id="nprogress"></div>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css"
type="text/css"
/>
<script src="https://app.lemonsqueezy.com/js/lemon.js"></script>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

View File

@@ -18,7 +18,6 @@
"@blueprintjs/timezone": "^4.5.43",
"@casl/ability": "^5.4.3",
"@casl/react": "^2.3.0",
"@craco/craco": "^5.9.0",
"@emotion/css": "^11.13.4",
"@emotion/react": "^11.13.3",
"@react-oauth/google": "^0.12.2",
@@ -36,7 +35,7 @@
"@tiptap/starter-kit": "2.1.13",
"@types/js-money": "^0.6.1",
"@types/lodash": "^4.14.172",
"@types/node": "^14.14.9",
"@types/node": "^20.16.10",
"@types/ramda": "^0.28.14",
"@types/react": "18.3.4",
"@types/react-body-classname": "^1.1.7",
@@ -61,13 +60,13 @@
"deepdash": "^5.3.9",
"dependency-graph": "^0.11.0",
"dotenv-webpack": "^8.0.1",
"esbuild-plugin-react-virtualized": "^1.0.5",
"eslint": "^8.33.0",
"fast-deep-equal": "^3.1.3",
"flat": "^5.0.2",
"formik": "^2.2.5",
"helmet": "^3.21.0",
"history": "4.10.1",
"http-proxy-middleware": "^1.0.0",
"js-cookie": "2.2.1",
"js-money": "^0.6.3",
"lodash": "^4.17.15",
@@ -81,16 +80,13 @@
"query-string": "^7.1.1",
"ramda": "^0.27.1",
"react": "^18.2.0",
"react-app-polyfill": "^1.0.6",
"react-body-classname": "^1.3.1",
"react-colorful": "^5.6.1",
"react-content-loader": "^6.0.1",
"react-dev-utils": "^11.0.4",
"react-dom": "^18.2.0",
"react-dropzone": "^11.0.1",
"react-dropzone-esm": "^15.0.1",
"react-error-boundary": "^3.0.2",
"react-error-overlay": "^6.0.9",
"react-helmet": "^6.1.0",
"react-hotkeys-hook": "^3.0.3",
"react-intl-universal": "^2.4.7",
@@ -102,7 +98,6 @@
"react-router": "5.3.4",
"react-router-breadcrumbs-hoc": "^3.2.10",
"react-router-dom": "^5.3.3",
"react-scripts": "5.0.1",
"react-scroll-sync": "^0.7.1",
"react-scrollbars-custom": "^4.0.21",
"react-sortablejs": "^2.0.11",
@@ -113,6 +108,7 @@
"react-use": "^13.26.1",
"react-use-context-menu": "^0.1.4",
"react-virtualized": "^9.22.3",
"regenerator-runtime": "^0.14.1",
"redux": "^4.2.1",
"redux-devtools": "^3.5.0",
"redux-persist": "^6.0.0",
@@ -129,17 +125,20 @@
"typescript": "^4.8.3",
"yup": "^0.28.1"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.4",
"@vitejs/plugin-legacy": "^5.4.2",
"eslint-config-react-app": "^7.0.1",
"vite": "^5.1.6"
},
"scripts": {
"dev": "cross-env PORT=4000 craco start",
"build": "craco build",
"dev": "cross-env PORT=4000 vite",
"build": "vite build",
"preview": "cross-env PORT=4173 vite preview",
"typecheck": "tsc --noEmit",
"test": "node scripts/test.js",
"storybook": "start-storybook -p 6006"
},
"proxy": "http://localhost:3000/",
"resolutions": {
"react-error-overlay": "6.0.9"
},
"eslintConfig": {
"extends": [
"react-app",

View File

@@ -1,56 +0,0 @@
<!doctype html>
<html dir="ltr" lang="en">
<head>
<meta charset="utf-8" />
<link
rel="icon"
href="%PUBLIC_URL%/favicons/favicon-32.ico"
sizes="32x32"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Bigcapital Financial Managment Software"
/>
<!-- <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> -->
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Bigcapital</title>
</head>
<body class="bp4-dark">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<div id="nprogress"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css"
type="text/css"
/>
<script src="https://app.lemonsqueezy.com/js/lemon.js"></script>
</body>
</html>

View File

@@ -1,11 +0,0 @@
const proxy = require('http-proxy-middleware');
module.exports = function(app) {
app.use(
'/api',
proxy({
target: 'http://localhost:3000',
changeOrigin: true,
})
);
};

View File

@@ -1,13 +1,43 @@
// @ts-nocheck
import React from 'react';
import { MenuItem } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import { FMultiSelect } from '../Forms';
import { accountPredicate } from './_components';
import { MenuItemNestedText } from '../Menu';
import { usePreprocessingAccounts } from './_hooks';
import { DialogsName } from '@/constants/dialogs';
import { useDialogActions } from '@/hooks/state/dashboard';
import { SelectOptionProps } from '@blueprintjs-formik/select';
interface Account {
id: number;
name: string;
code: string;
account_level: number;
account_type?: string;
account_parent_type?: string;
account_root_type?: string;
account_normal?: string;
}
export interface AccountSelect extends Partial<Account>, SelectOptionProps { }
type MultiSelectProps = React.ComponentProps<typeof FMultiSelect>;
interface AccountsMultiSelectProps extends Omit<MultiSelectProps, 'items'> {
items: AccountSelect[];
allowCreate?: boolean;
filterByRootTypes?: string[];
filterByParentTypes?: string[];
filterByTypes?: string[];
filterByNormal?: string[];
}
// Create new account renderer.
const createNewItemRenderer = (query, active, handleClick) => {
const createNewItemRenderer = (
query: string,
active: boolean,
handleClick: (event: React.MouseEvent<HTMLElement>) => void,
): React.ReactElement => {
return (
<MenuItem
icon="add"
@@ -18,32 +48,15 @@ const createNewItemRenderer = (query, active, handleClick) => {
);
};
/**
* Default account item renderer of the list.
* @returns {JSX.Element}
*/
const accountRenderer = (
item,
{ handleClick, modifiers, query },
{ isSelected },
) => {
if (!modifiers.matchesPredicate) {
return null;
}
return (
<MenuItem
active={modifiers.active}
disabled={modifiers.disabled}
text={<MenuItemNestedText level={item.account_level} text={item.name} />}
key={item.id}
onClick={handleClick}
icon={isSelected ? 'tick' : 'blank'}
/>
);
};
// Create new item from the given query string.
const createNewItemFromQuery = (name) => ({ name });
const createNewItemFromQuery = (query: string): AccountSelect => ({
label: query,
value: query,
text: query,
id: 0,
name: query,
code: query,
});
/**
* Accounts multi-select field binded with Formik form.
@@ -59,27 +72,32 @@ export function AccountsMultiSelect({
filterByNormal,
...rest
}) {
}: AccountsMultiSelectProps): React.ReactElement {
const { openDialog } = useDialogActions();
// Filters accounts based on filter props.
const filteredAccounts = usePreprocessingAccounts(items, {
filterByParentTypes,
filterByTypes,
filterByNormal,
filterByRootTypes,
filterByParentTypes: filterByParentTypes || [],
filterByTypes: filterByTypes || [],
filterByNormal: filterByNormal || [],
filterByRootTypes: filterByRootTypes || [],
});
// Maybe inject new item props to select component.
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
const maybeCreateNewItemRenderer = allowCreate
? createNewItemRenderer
: undefined;
const maybeCreateNewItemFromQuery = allowCreate
? createNewItemFromQuery
: null;
: undefined;
// Handles the create item click.
const handleCreateItemClick = () => {
const handleCreateItemClick = (): void => {
openDialog(DialogsName.AccountForm);
};
return (
<FMultiSelect
<FMultiSelect<AccountSelect>
{...rest}
items={filteredAccounts}
valueAccessor={'id'}
textAccessor={'name'}
@@ -87,11 +105,9 @@ export function AccountsMultiSelect({
tagAccessor={'name'}
popoverProps={{ minimal: true }}
itemPredicate={accountPredicate}
itemRenderer={accountRenderer}
createNewItemRenderer={maybeCreateNewItemRenderer}
createNewItemFromQuery={maybeCreateNewItemFromQuery}
onCreateItemSelect={handleCreateItemClick}
{...rest}
/>
);
}

View File

@@ -1,17 +1,19 @@
// @ts-nocheck
import React, { useState, useCallback, useEffect, useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';
import * as R from 'ramda';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import { MenuItem } from '@blueprintjs/core';
import { Suggest } from '@blueprintjs/select';
import { CLASSES } from '@/constants/classes';
import { DialogsName } from '@/constants/dialogs';
import { MenuItemNestedText, FormattedMessage as T } from '@/components';
import {
FSuggest,
MenuItemNestedText,
FormattedMessage as T,
} from '@/components';
import { nestedArrayToflatten, filterAccountsByQuery } from '@/utils';
import withDialogActions from '@/containers/Dialog/withDialogActions';
// Create new account renderer.
@@ -28,17 +30,7 @@ const createNewItemRenderer = (query, active, handleClick) => {
// Create new item from the given query string.
const createNewItemFromQuery = (name) => {
return {
name,
};
};
// Handle input value renderer.
const handleInputValueRenderer = (inputValue) => {
if (inputValue) {
return inputValue.name.toString();
}
return '';
return { name };
};
// Filters accounts items.
@@ -62,11 +54,7 @@ function AccountsSuggestFieldRoot({
// #ownProps
accounts,
initialAccountId,
selectedAccountId,
defaultSelectText = intl.formatMessage({ id: 'select_account' }),
popoverFill = false,
onAccountSelected,
filterByParentTypes = [],
filterByTypes = [],
@@ -81,67 +69,30 @@ function AccountsSuggestFieldRoot({
() => nestedArrayToflatten(accounts),
[accounts],
);
// Filters accounts based on filter props.
const filteredAccounts = useMemo(() => {
let filteredAccounts = filterAccountsByQuery(flattenAccounts, {
filterByRootTypes,
const filteredAccounts = useMemo(
() =>
filterAccountsByQuery(flattenAccounts, {
filterByParentTypes,
filterByTypes,
filterByNormal,
filterByRootTypes,
}),
[
flattenAccounts,
filterByParentTypes,
filterByTypes,
filterByNormal,
});
return filteredAccounts;
}, [
flattenAccounts,
filterByRootTypes,
filterByParentTypes,
filterByTypes,
filterByNormal,
]);
// Find initial account object to set it as default account in initial render.
const initialAccount = useMemo(
() => filteredAccounts.find((a) => a.id === initialAccountId),
[initialAccountId, filteredAccounts],
filterByRootTypes,
],
);
const [selectedAccount, setSelectedAccount] = useState(
initialAccount || null,
);
useEffect(() => {
if (typeof selectedAccountId !== 'undefined') {
const account = selectedAccountId
? filteredAccounts.find((a) => a.id === selectedAccountId)
: null;
setSelectedAccount(account);
}
}, [selectedAccountId, filteredAccounts, setSelectedAccount]);
// Account item of select accounts field.
const accountItem = useCallback((item, { handleClick, modifiers, query }) => {
return (
<MenuItem
text={<MenuItemNestedText level={item.level} text={item.name} />}
label={item.code}
key={item.id}
onClick={handleClick}
/>
);
}, []);
const handleAccountSelect = useCallback(
(account) => {
if (account.id) {
setSelectedAccount({ ...account });
onAccountSelected && onAccountSelected(account);
} else {
const handleCreateItemSelect = useCallback(
(item) => {
if (!item.id) {
openDialog(DialogsName.AccountForm);
}
},
[setSelectedAccount, onAccountSelected, openDialog],
[openDialog],
);
// Maybe inject new item props to select component.
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
const maybeCreateNewItemFromQuery = allowCreate
@@ -149,21 +100,15 @@ function AccountsSuggestFieldRoot({
: null;
return (
<Suggest
<FSuggest
items={filteredAccounts}
noResults={<MenuItem disabled={true} text={<T id={'no_accounts'} />} />}
itemRenderer={accountItem}
itemPredicate={filterAccountsPredicater}
onItemSelect={handleAccountSelect}
selectedItem={selectedAccount}
onCreateItemSelect={handleCreateItemSelect}
valueAccessor="id"
textAccessor="name"
labelAccessor="code"
inputProps={{ placeholder: defaultSelectText }}
resetOnClose={true}
fill={true}
resetOnClose
popoverProps={{ minimal: true, boundary: 'window' }}
inputValueRenderer={handleInputValueRenderer}
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
createNewItemRenderer={maybeCreateNewItemRenderer}
createNewItemFromQuery={maybeCreateNewItemFromQuery}
{...suggestProps}

View File

@@ -1,7 +1,14 @@
// @ts-nocheck
import { AccountSelect } from "./AccountsMultiSelect";
// Filters accounts items.
export const accountPredicate = (query, account, _index, exactMatch) => {
export const accountPredicate = (
query: string,
account: AccountSelect,
_index?: number,
exactMatch?: boolean,
) => {
const normalizedTitle = account.name.toLowerCase();
const normalizedQuery = query.toLowerCase();

View File

@@ -36,15 +36,19 @@ function getCurrentLocal() {
/**
* Loads the localization data of the given locale.
*/
function loadLocales(currentLocale) {
return import(`../lang/${currentLocale}/index.json`);
async function loadLocales(currentLocale) {
return await import(`../lang/${currentLocale}/index.json`).then(
(module) => module.default,
);
}
/**
* Loads the localization data of yup validation library.
*/
function loadYupLocales(currentLocale) {
return import(`../lang/${currentLocale}/locale`);
async function loadYupLocales(currentLocale) {
return await import(`../lang/${currentLocale}/locale.tsx`).then(
(module) => module.locale,
);
}
/**
@@ -109,11 +113,11 @@ function useAppYupLoadLocales(currentLocale) {
React.useEffect(() => {
loadYupLocales(currentLocale)
.then(({ locale }) => {
setLocale(locale);
.then((results) => {
setLocale(results);
setIsLoading(false);
})
.then(() => {});
.then(() => { });
}, [currentLocale, stopLoading]);
// Watches the valiue to start/stop splash screen.

View File

@@ -6,10 +6,14 @@ import { Select } from '@blueprintjs/select';
export function CurrenciesSelectList({ selectProps, onItemSelect, className }) {
const currencies = [
{
id: 'USD',
code: 'USD',
name: 'USD US dollars',
key: 'USD',
},
{
id: 'CAD',
code: 'CAD',
name: 'CAD Canadian dollars',
key: 'CAD',
},
];

View File

@@ -56,7 +56,7 @@ export function SidebarMenu({ menu }) {
<div>
<Menu className="sidebar-menu">
{menu.map((item, index) => (
<SidebarMenuItemComposer index={index} item={item} />
<SidebarMenuItemComposer key={index} index={index} item={item} />
))}
</Menu>
</div>

View File

@@ -2,20 +2,12 @@
import React from 'react';
import intl from 'react-intl-universal';
import styled from 'styled-components';
import { FastField, ErrorMessage, useFormikContext } from 'formik';
import { FastField, useFormikContext } from 'formik';
import { isEqual } from 'lodash';
import {
Classes,
FormGroup,
InputGroup,
TextArea,
Position,
ControlGroup,
} from '@blueprintjs/core';
import { Classes, Position, ControlGroup } from '@blueprintjs/core';
import { useAutofocus } from '@/hooks';
import classNames from 'classnames';
import { CLASSES, ACCOUNT_TYPE, Features } from '@/constants';
import { DateInput } from '@blueprintjs/datetime';
import {
FieldRequiredHint,
@@ -31,13 +23,13 @@ import {
ExchangeRateMutedField,
BranchSelect,
BranchSelectButton,
FFormGroup,
FInputGroup,
FDateInput,
FTextArea,
FMoneyInputGroup,
} from '@/components';
import {
inputIntent,
momentFormatter,
tansformDateValue,
handleDateChange,
} from '@/utils';
import { inputIntent, momentFormatter } from '@/utils';
import { useSetPrimaryBranchToForm } from './utils';
import { useQuickPaymentMadeContext } from './QuickPaymentMadeFormProvider';
@@ -66,93 +58,46 @@ function QuickPaymentMadeFormFields({
<FeatureCan feature={Features.Branches}>
<Row>
<Col xs={5}>
<FormGroup
label={<T id={'branch'} />}
className={classNames('form-group--select-list', Classes.FILL)}
>
<FFormGroup label={<T id={'branch'} />} name={'branch_id'}>
<BranchSelect
name={'branch_id'}
branches={branches}
input={BranchSelectButton}
popoverProps={{ minimal: true }}
/>
</FormGroup>
</FFormGroup>
</Col>
</Row>
<BranchRowDivider />
</FeatureCan>
<Row>
{/* ------------- Vendor name ------------- */}
<Col xs={5}>
{/* ------------- Vendor name ------------- */}
<FastField name={'vendor_id'}>
{({ from, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'vendor_name'} />}
className={classNames('form-group--select-list', CLASSES.FILL)}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'vendor'} />}
>
<InputGroup
intent={inputIntent({ error, touched })}
minimal={true}
disabled={true}
{...field}
/>
</FormGroup>
)}
</FastField>
<FFormGroup name={'vendor_id'} label={<T id={'vendor_name'} />}>
<FInputGroup name={'vendor_id'} minimal={true} disabled={true} />
</FFormGroup>
</Col>
{/* ------------ Payment number. ------------ */}
<Col xs={5}>
{/* ------------ Payment number. ------------ */}
<FastField name={'payment_number'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'payment_no'} />}
className={('form-group--payment_number', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="payment_number" />}
>
<InputGroup
intent={inputIntent({ error, touched })}
minimal={true}
{...field}
/>
</FormGroup>
)}
</FastField>
<FFormGroup name={'payment_number'} label={<T id={'payment_no'} />}>
<FInputGroup name={'payment_number'} minimal={true} />
</FFormGroup>
</Col>
</Row>
{/*------------ Amount Received -----------*/}
<FastField name={'amount'}>
{({
form: { values, setFieldValue },
field: { value },
meta: { error, touched },
}) => (
<FormGroup
label={<T id={'amount_received'} />}
labelInfo={<FieldRequiredHint />}
className={classNames('form-group--payment_amount', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="amount" />}
>
<ControlGroup>
<InputPrependText text={values.currency_code} />
<MoneyInputGroup
value={value}
minimal={true}
onChange={(amount) => {
setFieldValue('amount', amount);
}}
intent={inputIntent({ error, touched })}
inputRef={(ref) => (paymentMadeFieldRef.current = ref)}
/>
</ControlGroup>
</FormGroup>
)}
</FastField>
{/*------------ Amount Received -----------*/}
<FFormGroup name={'amount'} label={<T id={'amount_received'} />}>
<ControlGroup>
<InputPrependText text={values.currency_code} />
<FMoneyInputGroup
name={'amount'}
minimal={true}
inputRef={(ref) => (paymentMadeFieldRef.current = ref)}
/>
</ControlGroup>
</FFormGroup>
<If condition={!isEqual(base_currency, values.currency_code)}>
{/*------------ exchange rate -----------*/}
@@ -165,95 +110,58 @@ function QuickPaymentMadeFormFields({
exchangeRate={values.exchange_rate}
/>
</If>
<Row>
<Col xs={5}>
{/* ------------- Payment date ------------- */}
<FastField name={'payment_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'payment_date'} />}
labelInfo={<FieldRequiredHint />}
className={classNames('form-group--select-list', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="payment_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('payment_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
/>
</FormGroup>
)}
</FastField>
<FFormGroup
name={'payment_date'}
label={<T id={'payment_date'} />}
labelInfo={<FieldRequiredHint />}
className={classNames('form-group--select-list', CLASSES.FILL)}
>
<FDateInput
name={'payment_date'}
{...momentFormatter('YYYY/MM/DD')}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
/>
</FFormGroup>
</Col>
<Col xs={5}>
{/* ------------ payment account ------------ */}
<FastField name={'payment_account_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'payment_account'} />}
className={classNames(
'form-group--payment_account_id',
'form-group--select-list',
CLASSES.FILL,
)}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'payment_account_id'} />}
>
<AccountsSuggestField
accounts={accounts}
onAccountSelected={({ id }) =>
form.setFieldValue('payment_account_id', id)
}
inputProps={{
placeholder: intl.get('select_account'),
}}
filterByTypes={[
ACCOUNT_TYPE.CASH,
ACCOUNT_TYPE.BANK,
ACCOUNT_TYPE.OTHER_CURRENT_ASSET,
]}
/>
</FormGroup>
)}
</FastField>
<FFormGroup
name={'payment_account_id'}
label={<T id={'payment_account'} />}
>
<AccountsSuggestField
name={'payment_account_id'}
accounts={accounts}
inputProps={{
placeholder: intl.get('select_account'),
}}
filterByTypes={[
ACCOUNT_TYPE.CASH,
ACCOUNT_TYPE.BANK,
ACCOUNT_TYPE.OTHER_CURRENT_ASSET,
]}
/>
</FFormGroup>
</Col>
</Row>
{/* ------------ Reference No. ------------ */}
<FastField name={'reference'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'reference'} />}
className={classNames('form-group--reference', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="reference" />}
>
<InputGroup
intent={inputIntent({ error, touched })}
minimal={true}
{...field}
/>
</FormGroup>
)}
</FastField>
<FFormGroup name={'reference'} label={<T id={'reference'} />}>
<FInputGroup name={'reference'} minimal={true} />
</FFormGroup>
{/* --------- Statement --------- */}
<FastField name={'statement'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'statement'} />}
className={'form-group--statement'}
>
<TextArea growVertically={true} {...field} />
</FormGroup>
)}
</FastField>
<FFormGroup name={'statement'} label={<T id={'statement'} />}>
<FTextArea name={'statement'} growVertically={true} fill={true} />
</FFormGroup>
</div>
);
}
@@ -264,4 +172,8 @@ export const BranchRowDivider = styled.div`
height: 1px;
background: #ebf1f6;
margin-bottom: 15px;
.bp4-dark &{
background: rgba(255, 255, 255, 0.1);
}
`;

View File

@@ -5,17 +5,9 @@ import intl from 'react-intl-universal';
import { FastField, ErrorMessage, useFormikContext } from 'formik';
import { useAutofocus } from '@/hooks';
import { isEqual } from 'lodash';
import {
Classes,
FormGroup,
InputGroup,
TextArea,
Position,
ControlGroup,
} from '@blueprintjs/core';
import { Classes, Position, ControlGroup } from '@blueprintjs/core';
import classNames from 'classnames';
import { CLASSES, Features, ACCOUNT_TYPE } from '@/constants';
import { DateInput } from '@blueprintjs/datetime';
import {
Row,
Col,
@@ -30,17 +22,15 @@ import {
ExchangeRateMutedField,
BranchSelect,
BranchSelectButton,
FFormGroup,
FInputGroup,
FTextArea,
FDateInput,
FMoneyInputGroup,
} from '@/components';
import {
inputIntent,
momentFormatter,
tansformDateValue,
handleDateChange,
compose,
} from '@/utils';
import { momentFormatter, compose } from '@/utils';
import { useSetPrimaryBranchToForm } from './utils';
import { useQuickPaymentReceiveContext } from './QuickPaymentReceiveFormProvider';
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
import withSettings from '@/containers/Settings/withSettings';
@@ -68,95 +58,57 @@ function QuickPaymentReceiveFormFields({
<FeatureCan feature={Features.Branches}>
<Row>
<Col xs={5}>
<FormGroup
label={<T id={'branch'} />}
className={classNames('form-group--select-list', Classes.FILL)}
>
<FFormGroup name={'branch_id'} label={<T id={'branch'} />}>
<BranchSelect
name={'branch_id'}
branches={branches}
input={BranchSelectButton}
popoverProps={{ minimal: true }}
/>
</FormGroup>
</FFormGroup>
</Col>
</Row>
<BranchRowDivider />
</FeatureCan>
<Row>
<Col xs={5}>
{/* ------------- Customer name ------------- */}
<FastField name={'customer_id'}>
{({ from, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'customer_name'} />}
className={classNames('form-group--select-list', CLASSES.FILL)}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'customer_id'} />}
>
<InputGroup
intent={inputIntent({ error, touched })}
minimal={true}
disabled={true}
{...field}
/>
</FormGroup>
)}
</FastField>
<FFormGroup
name={'customer_id'}
label={<T id={'customer_name'} />}
labelInfo={<FieldRequiredHint />}
>
<FInputGroup name={'customer_id'} minimal={true} disabled={true} />
</FFormGroup>
</Col>
<Col xs={5}>
{/* ------------ Payment receive no. ------------ */}
<FastField name={'payment_receive_no'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'payment_no'} />}
labelInfo={<FieldRequiredHint />}
className={('form-group--payment_receive_no', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="payment_receive_no" />}
>
<InputGroup
intent={inputIntent({ error, touched })}
minimal={true}
{...field}
disabled={paymentReceiveAutoIncrement}
/>
</FormGroup>
)}
</FastField>
<FFormGroup
name={'payment_receive_no'}
label={<T id={'payment_no'} />}
>
<FInputGroup
name={'payment_receive_no'}
minimal={true}
disabled={paymentReceiveAutoIncrement}
/>
</FFormGroup>
</Col>
</Row>
{/*------------ Amount Received -----------*/}
<FastField name={'amount'}>
{({
form: { values, setFieldValue },
field: { value },
meta: { error, touched },
}) => (
<FormGroup
label={<T id={'amount_received'} />}
labelInfo={<FieldRequiredHint />}
className={classNames('form-group--payment_amount', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="amount" />}
>
<ControlGroup>
<InputPrependText text={values.currency_code} />
<MoneyInputGroup
value={value}
minimal={true}
onChange={(amount) => {
setFieldValue('amount', amount);
}}
intent={inputIntent({ error, touched })}
inputRef={(ref) => (paymentReceiveFieldRef.current = ref)}
/>
</ControlGroup>
</FormGroup>
)}
</FastField>
<FFormGroup name={'amount'} label={<T id={'amount_received'} />}>
<ControlGroup>
<InputPrependText text={values.currency_code} />
<FMoneyInputGroup
name={'amount'}
minimal={true}
inputRef={(ref) => (paymentReceiveFieldRef.current = ref)}
/>
</ControlGroup>
</FFormGroup>
<If condition={!isEqual(base_currency, values.currency_code)}>
{/*------------ exchange rate -----------*/}
@@ -173,94 +125,53 @@ function QuickPaymentReceiveFormFields({
<Row>
<Col xs={5}>
{/* ------------- Payment date ------------- */}
<FastField name={'payment_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'payment_date'} />}
labelInfo={<FieldRequiredHint />}
className={classNames('form-group--select-list', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="payment_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('payment_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
/>
</FormGroup>
)}
</FastField>
<FFormGroup name={'payment_date'} label={<T id={'payment_date'} />}>
<FDateInput
{...momentFormatter('YYYY/MM/DD')}
name={'payment_date'}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
/>
</FFormGroup>
</Col>
<Col xs={5}>
{/* ------------ Deposit account ------------ */}
<FastField name={'deposit_account_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'deposit_to'} />}
className={classNames(
'form-group--deposit_account_id',
'form-group--select-list',
CLASSES.FILL,
)}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'deposit_account_id'} />}
>
<AccountsSuggestField
selectedAccountId={value}
accounts={accounts}
onAccountSelected={({ id }) =>
form.setFieldValue('deposit_account_id', id)
}
inputProps={{
placeholder: intl.get('select_account'),
}}
filterByTypes={[
ACCOUNT_TYPE.CASH,
ACCOUNT_TYPE.BANK,
ACCOUNT_TYPE.OTHER_CURRENT_ASSET,
]}
/>
</FormGroup>
)}
</FastField>
<FFormGroup
name={'deposit_account_id'}
label={<T id={'deposit_to'} />}
>
<AccountsSuggestField
name={'deposit_account_id'}
accounts={accounts}
inputProps={{
placeholder: intl.get('select_account'),
}}
filterByTypes={[
ACCOUNT_TYPE.CASH,
ACCOUNT_TYPE.BANK,
ACCOUNT_TYPE.OTHER_CURRENT_ASSET,
]}
/>
</FFormGroup>
</Col>
</Row>
{/* ------------ Reference No. ------------ */}
<FastField name={'reference_no'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'reference'} />}
className={classNames('form-group--reference', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="reference" />}
>
<InputGroup
intent={inputIntent({ error, touched })}
minimal={true}
{...field}
/>
</FormGroup>
)}
</FastField>
<FFormGroup label={<T id={'reference'} />} name={'reference_no'}>
<FInputGroup name={'reference_no'} minimal={true} />
</FFormGroup>
{/* --------- Statement --------- */}
<FastField name={'statement'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'statement'} />}
className={'form-group--statement'}
>
<TextArea growVertically={true} {...field} />
</FormGroup>
)}
</FastField>
<FFormGroup
name={'statement'}
label={<T id={'statement'} />}
className={'form-group--statement'}
>
<FTextArea name={'statement'} growVertically={true} />
</FFormGroup>
</div>
);
}
@@ -276,4 +187,8 @@ export const BranchRowDivider = styled.div`
height: 1px;
background: #ebf1f6;
margin-bottom: 15px;
.bp4-dark & {
background: rgba(255, 255, 255, 0.1);
}
`;

View File

@@ -34,12 +34,9 @@ function BulkDeleteDialogContent({
</Tag>
<x.div>
<T
id={'bulk_delete_delete_row_prefix'}
id={'bulk_delete_will_be_deleted'}
values={{ resourceSingular: resourceSingularLabel }}
/>{' '}
<x.span fontWeight="semibold" color="danger">
<T id={'bulk_delete_delete_row_status'} />
</x.span>
</x.div>
</x.div>
@@ -49,12 +46,9 @@ function BulkDeleteDialogContent({
</Tag>
<x.div>
<T
id={'bulk_delete_archive_row_prefix'}
id={'bulk_delete_cannot_be_deleted'}
values={{ resourceSingular: resourceSingularLabel }}
/>{' '}
<x.span fontWeight="semibold" color="info">
<T id={'bulk_delete_archive_row_status'} />
</x.span>
</x.div>
</x.div>

View File

@@ -3,6 +3,7 @@ import { useMutation, useQueryClient } from 'react-query';
import { useRequestQuery } from '../useQueryRequest';
import useApiRequest from '../useRequest';
import t from './types';
import { transformToCamelCase } from '@/utils';
// Transform the account.
const transformAccount = (response) => {
@@ -187,7 +188,9 @@ export function useValidateBulkDeleteAccounts(props) {
return useMutation(
(ids: number[]) =>
apiRequest.post('accounts/validate-bulk-delete', { ids }),
apiRequest
.post('accounts/validate-bulk-delete', { ids })
.then((res) => transformToCamelCase(res.data)),
{
...props,
},

View File

@@ -2,7 +2,7 @@
import { useMutation, useQueryClient } from 'react-query';
import useApiRequest from '../useRequest';
import { useRequestQuery } from '../useQueryRequest';
import { transformPagination } from '@/utils';
import { transformPagination, transformToCamelCase } from '@/utils';
import t from './types';
const defaultPagination = {

View File

@@ -1,7 +1,7 @@
// @ts-nocheck
import { useMutation, useQueryClient } from 'react-query';
import { useRequestQuery } from '../useQueryRequest';
import { transformPagination } from '@/utils';
import { transformPagination, transformToCamelCase } from '@/utils';
import useApiRequest from '../useRequest';
import t from './types';

View File

@@ -1,4 +1,6 @@
// @ts-nocheck
import 'regenerator-runtime/runtime';
import './wdyr';
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
@@ -10,11 +12,6 @@ import App from '@/components/App';
import * as serviceWorker from '@/serviceWorker';
import { store, persistor } from '@/store/createStore';
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, { trackAllPureComponents: false });
}
ReactDOM.render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>

View File

@@ -529,10 +529,8 @@
"once_delete_these_invoices_you_will_not_able_restore_them": "Once you delete these invoices, you won't be able to retrieve them later. Are you sure you want to delete them?",
"bulk_delete_dialog_title": "Delete {resourcePlural}",
"bulk_delete_selected_summary": "You have selected {count} {resourcePlural} to be deleted:",
"bulk_delete_delete_row_prefix": "{resourceSingular} will be",
"bulk_delete_delete_row_status": "Deleted",
"bulk_delete_archive_row_prefix": "{resourceSingular} cannot be deleted so it will be",
"bulk_delete_archive_row_status": "Archived",
"bulk_delete_will_be_deleted": "{resourceSingular} will be deleted",
"bulk_delete_cannot_be_deleted": "{resourceSingular} cannot be deleted",
"bulk_delete_selected_description": "These {resourcePlural} will be removed from any contacts or workflows using them as a default document.",
"bulk_delete_note_description": "Deleting {resourcePlural} is permanent and cannot be undone. Archived {resourcePlural} can be restored at any time.",
"resource_invoice_singular": "Invoice",
@@ -2310,5 +2308,6 @@
"api_key.important": "Important",
"api_key.display_warning": "This API key will only be shown once. Please copy and save it securely.",
"api_key.label": "API Key",
"api_key.copy_to_clipboard": "Copy to clipboard"
"api_key.copy_to_clipboard": "Copy to clipboard",
"the_expenses_has_been_deleted_successfully": "The expenses have been deleted successfully."
}

View File

@@ -62,9 +62,6 @@ const initialState = {
warehouseTransfer: {
tableSize: 'small',
},
projectTasks: {
tableSize: 'small',
},
projectTasks: {
tableSize: 'medium',
},

2
packages/webapp/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,8 @@
import React from 'react';
import whyDidYouRender from '@welldone-software/why-did-you-render';
if (process.env.NODE_ENV === 'development') {
whyDidYouRender(React, {
trackAllPureComponents: true,
});
}

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"target": "ESNext",
"lib": [
"dom",
"dom.iterable",
@@ -13,7 +13,7 @@
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
@@ -23,5 +23,10 @@
"include": [
"src"
],
"references": [
{
"path": "./tsconfig.node.json"
}
],
"extends": "./tsconfig.base.json"
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,68 @@
import react from '@vitejs/plugin-react';
import legacy from '@vitejs/plugin-legacy';
import path from 'node:path';
import { defineConfig, loadEnv, type PluginOption } from 'vite';
import fixReactVirtualized from 'esbuild-plugin-react-virtualized';
const allowedEnvPrefixes = ['VITE_', 'REACT_APP_', 'PUBLIC_URL'];
const pickClientEnv = (env: Record<string, string>) =>
Object.keys(env).reduce<Record<string, string>>((acc, key) => {
if (allowedEnvPrefixes.some((prefix) => key.startsWith(prefix))) {
acc[key] = env[key];
}
return acc;
}, {});
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
const rootDir = __dirname;
const env = loadEnv(mode, rootDir, '');
const clientEnv = pickClientEnv(env);
const port = Number(env.PORT) || 4000;
const plugins: PluginOption[] = [
react(),
legacy({
targets: ['defaults', 'not IE 11'],
additionalLegacyPolyfills: ['regenerator-runtime/runtime'],
}),
];
return {
plugins,
root: rootDir,
resolve: {
alias: {
'@': path.resolve(rootDir, 'src'),
'@public': path.resolve(rootDir, 'public'),
path: 'path-browserify',
},
},
define: {
'process.env': {
NODE_ENV: mode,
PUBLIC_URL: clientEnv.PUBLIC_URL ?? '/',
...clientEnv,
},
},
server: {
host: true,
port,
proxy: {
'/api': {
target: env.VITE_API_PROXY_TARGET || 'http://localhost:3000',
changeOrigin: true,
},
},
},
build: {
outDir: 'dist',
},
optimizeDeps: {
esbuildOptions: {
plugins: [fixReactVirtualized as any],
},
},
};
});

8854
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"installCommand": "pnpm install",
"buildCommand": "CI='' pnpm run build:webapp",
"outputDirectory": "packages/webapp/build",
"outputDirectory": "packages/webapp/dist",
"rewrites": [{
"source": "/api/:slug*",
"destination": "https://dev.bigcapital.ly/api/:slug*"