Compare commits

...

22 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
a142b734d3 fix: remove the ignore script from vercel. 2023-03-06 01:54:51 +02:00
a.bouhuolia
2263cf5657 fix(webapp): add icon to duplicate item of items context menu 2023-02-16 22:34:14 +02:00
Ahmed Bouhuolia
e488c0eea9 Merge pull request #86 from bigcapitalhq/BIG-421-account-form-issues
fix: BIG-421 account form issues
2023-02-15 21:55:03 +02:00
a.bouhuolia
f093239a15 fix(webapp): retrieve nested graph accounts 2023-02-15 21:53:50 +02:00
a.bouhuolia
5c537e094d fix(server): retrieve nested graph accounts 2023-02-15 21:53:13 +02:00
a.bouhuolia
a371fd44f7 chore: update package-lock.json 2023-02-15 00:01:46 +02:00
Ahmed Bouhuolia
59cb168331 Merge pull request #85 from bigcapitalhq/BIG-414-control-max-nested-accounts-to-be-6-levels
feat(server): validate the max depth level of the parent account.
2023-02-14 23:48:45 +02:00
a.bouhuolia
8a5fbfc041 feat(server): validate the max depth level of the parent account. 2023-02-14 23:47:24 +02:00
Ahmed Bouhuolia
e3a072e267 Merge pull request #84 from bigcapitalhq/BIG-406-accounts-chart-lags-scroll-down
fix(webapp): accounts chart lags scroll down
2023-02-14 23:25:43 +02:00
a.bouhuolia
b03606406e fix(webapp): accounts chart lags scroll down 2023-02-14 23:20:01 +02:00
a.bouhuolia
a1a7ee2b5b chore: add ignoreCommand to vercel configure 2023-02-13 21:38:37 +02:00
a.bouhuolia
228ae71a1c Merge https://github.com/bigcapitalhq/client into develop 2023-02-13 21:26:59 +02:00
a.bouhuolia
71a8d3e77f chore: add file to vercel 2023-02-13 21:26:46 +02:00
Ahmed Bouhuolia
4ddeb927cc Merge pull request #83 from bigcapitalhq/bigcapital-cli
feat(server): bigcapital cli commands
2023-02-13 20:51:03 +02:00
a.bouhuolia
72c1685fa6 feat(server): move all cli commands codebase to be TS based. 2023-02-13 20:47:09 +02:00
a.bouhuolia
7e7ee24109 feat(server): bigcapital cli commands 2023-02-09 23:38:13 +02:00
Ahmed Bouhuolia
708d971717 ci: change webapp package name. 2023-02-08 23:35:41 +02:00
Ahmed Bouhuolia
7781d092ca ci: webapp Github actions (#81) 2023-02-08 23:33:03 +02:00
a.bouhuolia
d0e84fb51a chore: update vercel config 2023-02-08 00:02:43 +02:00
a.bouhuolia
0e673ffa7c chore: update Vercel config 2023-02-08 00:00:49 +02:00
a.bouhuolia
4c4c73db2d chore: add Vercel config file. 2023-02-07 23:57:52 +02:00
a.bouhuolia
0086ee5186 chore: change build script 2023-02-07 23:30:46 +02:00
27 changed files with 6308 additions and 174 deletions

View File

@@ -19,7 +19,7 @@ on:
env: env:
REGISTRY: ghcr.io REGISTRY: ghcr.io
IMAGE_NAME: abouhuolia/bigcapital-client IMAGE_NAME: abouhuolia/bigcapital-webapp
jobs: jobs:
setup-build-publish-deploy: setup-build-publish-deploy:
@@ -50,8 +50,9 @@ jobs:
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
with: with:
context: . context: .
file: ./packages/webapp/Dockerfile
push: true push: true
tags: ghcr.io/bigcapitalhq/client:latest tags: ghcr.io/bigcapitalhq/webapp:latest
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
# Send notification to Slack channel. # Send notification to Slack channel.
- name: Slack Notification built and published successfully. - name: Slack Notification built and published successfully.

5
.vercelignore Normal file
View File

@@ -0,0 +1,5 @@
/*
!package.json
!package-lock.json
!yarn.lock
!packages/webapp

5697
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -6,9 +6,9 @@
"dev": "lerna run dev", "dev": "lerna run dev",
"build": "lerna run build", "build": "lerna run build",
"dev:webapp": "lerna run dev --scope \"@bigcapital/webapp\"", "dev:webapp": "lerna run dev --scope \"@bigcapital/webapp\"",
"build:webapp": "lerna run dev --scope \"@bigcapital/webapp\"", "build:webapp": "lerna run build --scope \"@bigcapital/webapp\"",
"dev:server": "lerna run dev --scope \"@bigcapital/server\"", "dev:server": "lerna run dev --scope \"@bigcapital/server\"",
"build:server": "lerna run dev --scope \"@bigcapital/server\"", "build:server": "lerna run build --scope \"@bigcapital/server\"",
"prepare": "husky install" "prepare": "husky install"
}, },
"workspaces": [ "workspaces": [

View File

@@ -8,7 +8,9 @@
"clear": "rimraf build", "clear": "rimraf build",
"dev": "cross-env NODE_ENV=development webpack --config scripts/webpack.config.js", "dev": "cross-env NODE_ENV=development webpack --config scripts/webpack.config.js",
"build:resources": "gulp --gulpfile=scripts/gulpfile.js styles styles-rtl", "build:resources": "gulp --gulpfile=scripts/gulpfile.js styles styles-rtl",
"build": "cross-env NODE_ENV=production webpack --config scripts/webpack.config.js", "build:app": "cross-env NODE_ENV=production webpack --config scripts/webpack.config.js",
"build:commands": "cross-env NODE_ENV=production webpack --config scripts/webpack.cli.js",
"build": "npm-run-all build:*",
"lint:fix": "eslint --fix ./**/*.ts" "lint:fix": "eslint --fix ./**/*.ts"
}, },
"author": "Ahmed Bouhuolia, <a.bouhuolia@gmail.com>", "author": "Ahmed Bouhuolia, <a.bouhuolia@gmail.com>",

View File

@@ -0,0 +1,11 @@
const { getCommonWebpackOptions } = require('./webpack.common');
const inputEntry = './src/commands/index.ts';
const outputDir = '../build';
const outputFilename = 'commands.js';
module.exports = getCommonWebpackOptions({
inputEntry,
outputDir,
outputFilename,
});

View File

@@ -0,0 +1,76 @@
const path = require('path');
const { NormalModuleReplacementPlugin } = require('webpack');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin');
const nodeExternals = require('webpack-node-externals');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const isDev = process.env.NODE_ENV === 'development';
exports.getCommonWebpackOptions = ({
inputEntry,
outputDir,
outputFilename,
}) => {
const webpackOptions = {
entry: ['regenerator-runtime/runtime', inputEntry],
target: 'node',
mode: isDev ? 'development' : 'production',
watch: isDev,
watchOptions: {
aggregateTimeout: 200,
poll: 1000,
},
output: {
path: path.resolve(__dirname, outputDir),
filename: outputFilename,
},
resolve: {
extensions: ['.ts', '.tsx', '.js'],
extensionAlias: {
'.ts': ['.js', '.ts'],
'.cts': ['.cjs', '.cts'],
'.mts': ['.mjs', '.mts'],
},
plugins: [
new TsconfigPathsPlugin({
configFile: './tsconfig.json',
extensions: ['.ts', '.tsx', '.js'],
}),
],
},
plugins: [
// Ignore knex dynamic required dialects that we don't use
new NormalModuleReplacementPlugin(
/m[sy]sql2?|oracle(db)?|sqlite3|pg-(native|query)/,
'noop2'
),
new ProgressBarPlugin(),
],
externals: [nodeExternals(), 'aws-sdk', 'prettier'],
module: {
rules: [
{
test: /\.([cm]?ts|tsx|js)$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true,
configFile: 'tsconfig.json',
},
},
],
exclude: /(node_modules)/,
},
],
},
};
if (isDev) {
webpackOptions.plugins.push(
new RunScriptWebpackPlugin({ name: outputFilename })
);
}
return webpackOptions;
};

View File

@@ -1,74 +1,11 @@
const path = require('path'); const { getCommonWebpackOptions } = require('./webpack.common');
const { NormalModuleReplacementPlugin } = require('webpack');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin');
const nodeExternals = require('webpack-node-externals');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const isDev = process.env.NODE_ENV === 'development'; const inputEntry = './src/server.ts';
const outputDir = '../build'; const outputDir = '../build';
const outputFilename = 'index.js'; const outputFilename = 'index.js';
const inputEntry = './src/server.ts';
const webpackOptions = { module.exports = getCommonWebpackOptions({
entry: ['regenerator-runtime/runtime', inputEntry], inputEntry,
target: 'node', outputDir,
mode: isDev ? 'development' : 'production', outputFilename,
watch: isDev, });
watchOptions: {
aggregateTimeout: 200,
poll: 1000,
},
output: {
path: path.resolve(__dirname, outputDir),
filename: outputFilename,
},
resolve: {
extensions: ['.ts', '.tsx', '.js'],
extensionAlias: {
'.ts': ['.js', '.ts'],
'.cts': ['.cjs', '.cts'],
'.mts': ['.mjs', '.mts'],
},
plugins: [
new TsconfigPathsPlugin({
configFile: './tsconfig.json',
extensions: ['.ts', '.tsx', '.js'],
}),
],
},
plugins: [
// Ignore knex dynamic required dialects that we don't use
new NormalModuleReplacementPlugin(
/m[sy]sql2?|oracle(db)?|sqlite3|pg-(native|query)/,
'noop2'
),
new ProgressBarPlugin(),
],
externals: [nodeExternals(), 'aws-sdk', 'prettier'],
module: {
rules: [
{
test: /\.([cm]?ts|tsx|js)$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true,
configFile: 'tsconfig.json',
},
},
],
exclude: /(node_modules)/,
},
],
},
};
if (isDev) {
webpackOptions.plugins.push(
new RunScriptWebpackPlugin({ name: outputFilename })
);
}
module.exports = webpackOptions;

View File

@@ -9,6 +9,7 @@ import DynamicListingService from '@/services/DynamicListing/DynamicListService'
import { DATATYPES_LENGTH } from '@/data/DataTypes'; import { DATATYPES_LENGTH } from '@/data/DataTypes';
import CheckPolicies from '@/api/middleware/CheckPolicies'; import CheckPolicies from '@/api/middleware/CheckPolicies';
import { AccountsApplication } from '@/services/Accounts/AccountsApplication'; import { AccountsApplication } from '@/services/Accounts/AccountsApplication';
import { MAX_ACCOUNTS_CHART_DEPTH } from 'services/Accounts/constants';
@Service() @Service()
export default class AccountsController extends BaseController { export default class AccountsController extends BaseController {
@@ -494,6 +495,22 @@ export default class AccountsController extends BaseController {
} }
); );
} }
if (error.errorType === 'PARENT_ACCOUNT_EXCEEDED_THE_DEPTH_LEVEL') {
return res.boom.badRequest(
'The parent account exceeded the depth level of accounts chart.',
{
errors: [
{
type: 'PARENT_ACCOUNT_EXCEEDED_THE_DEPTH_LEVEL',
code: 1500,
data: {
maxDepth: MAX_ACCOUNTS_CHART_DEPTH,
},
},
],
}
);
}
} }
next(error); next(error);
} }

View File

@@ -0,0 +1,282 @@
#!/usr/bin/env node
import commander from 'commander';
import color from 'colorette';
import argv from 'getopts';
import Knex from 'knex';
import { knexSnakeCaseMappers } from 'objection';
import config from '../config';
function initSystemKnex() {
return Knex({
client: config.system.db_client,
connection: {
host: config.system.db_host,
user: config.system.db_user,
password: config.system.db_password,
database: config.system.db_name,
charset: 'utf8',
},
migrations: {
directory: config.system.migrations_dir,
},
seeds: {
directory: config.system.seeds_dir,
},
pool: { min: 0, max: 7 },
...knexSnakeCaseMappers({ upperCase: true }),
});
}
function initTenantKnex(organizationId) {
return Knex({
client: config.tenant.db_client,
connection: {
host: config.tenant.db_host,
user: config.tenant.db_user,
password: config.tenant.db_password,
database: `${config.tenant.db_name_prefix}${organizationId}`,
charset: config.tenant.charset,
},
migrations: {
directory: config.tenant.migrations_dir,
},
seeds: {
directory: config.tenant.seeds_dir,
},
pool: { min: 0, max: 5 },
...knexSnakeCaseMappers({ upperCase: true }),
});
}
function exit(text) {
if (text instanceof Error) {
console.error(
color.red(`${text.detail ? `${text.detail}\n` : ''}${text.stack}`)
);
} else {
console.error(color.red(text));
}
process.exit(1);
}
function success(text) {
console.log(text);
process.exit(0);
}
function log(text) {
console.log(text);
}
function getAllSystemTenants(knex) {
return knex('tenants');
}
// module.exports = {
// log,
// success,
// exit,
// initSystemKnex,
// };
// - bigcapital system:migrate:latest
// - bigcapital system:migrate:rollback
// - bigcapital tenants:migrate:latest
// - bigcapital tenants:migrate:latest --tenant_id=XXX
// - bigcapital tenants:migrate:rollback
// - bigcapital tenants:migrate:rollback --tenant_id=XXX
// - bigcapital tenants:migrate:make
// - bigcapital system:migrate:make
// - bigcapital tenants:list
commander
.command('system:migrate:rollback')
.description('Migrate the system database of the application.')
.action(async () => {
try {
const sysKnex = await initSystemKnex();
const [batchNo, _log] = await sysKnex.migrate.rollback();
if (_log.length === 0) {
success(color.cyan('Already at the base migration'));
}
success(
color.green(`Batch ${batchNo} rolled back: ${_log.length} migrations`) +
(argv.verbose ? `\n${color.cyan(_log.join('\n'))}` : '')
);
} catch (error) {
exit(error);
}
});
commander
.command('system:migrate:latest')
.description('Migrate latest mgiration of the system database.')
.action(async () => {
try {
const sysKnex = await initSystemKnex();
const [batchNo, log] = await sysKnex.migrate.latest();
if (log.length === 0) {
success(color.cyan('Already up to date'));
}
success(
color.green(`Batch ${batchNo} run: ${log.length} migrations`) +
(argv.verbose ? `\n${color.cyan(log.join('\n'))}` : '')
);
} catch (error) {
exit(error);
}
});
commander
.command('system:migrate:make <name>')
.description('Created a named migration file to the system database.')
.action(async (name) => {
const sysKnex = await initSystemKnex();
sysKnex.migrate
.make(name)
.then((name) => {
success(color.green(`Created Migration: ${name}`));
})
.catch(exit);
});
commander
.command('tenants:list')
.description('Retrieve a list of all system tenants databases.')
.action(async (cmd) => {
try {
const sysKnex = await initSystemKnex();
const tenants = await getAllSystemTenants(sysKnex);
tenants.forEach((tenant) => {
const dbName = `${config.tenant.db_name_prefix}${tenant.organizationId}`;
console.log(
`ID: ${tenant.id} | Organization ID: ${tenant.organizationId} | DB Name: ${dbName}`
);
});
} catch (error) {
exit(error);
}
success('---');
});
commander
.command('tenants:migrate:make <name>')
.description('Created a named migration file to the tenants database.')
.action(async (name) => {
const sysKnex = await initTenantKnex();
sysKnex.migrate
.make(name)
.then((name) => {
success(color.green(`Created Migration: ${name}`));
})
.catch(exit);
});
commander
.command('tenants:migrate:latest')
.description('Migrate all tenants or the given tenant id.')
.option('-t, --tenant_id [tenant_id]', 'Which tenant id do you migrate.')
.action(async (cmd) => {
try {
const sysKnex = await initSystemKnex();
const tenants = await getAllSystemTenants(sysKnex);
const tenantsOrgsIds = tenants.map((tenant) => tenant.organizationId);
if (cmd.tenant_id && tenantsOrgsIds.indexOf(cmd.tenant_id) === -1) {
exit(`The given tenant id ${cmd.tenant_id} is not exists.`);
}
// Validate the tenant id exist first of all.
const migrateOpers = [];
const migrateTenant = async (organizationId) => {
try {
const tenantKnex = await initTenantKnex(organizationId);
const [batchNo, _log] = await tenantKnex.migrate.latest();
const tenantDb = `${config.tenant.db_name_prefix}${organizationId}`;
if (_log.length === 0) {
log(color.cyan('Already up to date'));
}
log(
color.green(
`Tenant ${tenantDb} > Batch ${batchNo} run: ${_log.length} migrations`
) + (argv.verbose ? `\n${color.cyan(log.join('\n'))}` : '')
);
log('-------------------');
} catch (error) {
log(error);
}
};
if (!cmd.tenant_id) {
tenants.forEach((tenant) => {
const oper = migrateTenant(tenant.organizationId);
migrateOpers.push(oper);
});
} else {
const oper = migrateTenant(cmd.tenant_id);
migrateOpers.push(oper);
}
Promise.all(migrateOpers).then(() => {
success('All tenants are migrated.');
});
} catch (error) {
exit(error);
}
});
commander
.command('tenants:migrate:rollback')
.description('Rollback the last batch of tenants migrations.')
.option('-t, --tenant_id [tenant_id]', 'Which tenant id do you migrate.')
.action(async (cmd) => {
try {
const sysKnex = await initSystemKnex();
const tenants = await getAllSystemTenants(sysKnex);
const tenantsOrgsIds = tenants.map((tenant) => tenant.organizationId);
if (cmd.tenant_id && tenantsOrgsIds.indexOf(cmd.tenant_id) === -1) {
exit(`The given tenant id ${cmd.tenant_id} is not exists.`);
}
const migrateOpers = [];
const migrateTenant = async (organizationId) => {
try {
const tenantKnex = await initTenantKnex(organizationId);
const [batchNo, _log] = await tenantKnex.migrate.rollback();
const tenantDb = `${config.tenant.db_name_prefix}${organizationId}`;
if (_log.length === 0) {
log(color.cyan('Already at the base migration'));
}
log(
color.green(
`Tenant: ${tenantDb} > Batch ${batchNo} rolled back: ${_log.length} migrations`
) + (argv.verbose ? `\n${color.cyan(_log.join('\n'))}` : '')
);
log('---------------');
} catch (error) {
exit(error);
}
};
if (!cmd.tenant_id) {
tenants.forEach((tenant) => {
const oper = migrateTenant(tenant.organizationId);
migrateOpers.push(oper);
});
} else {
const oper = migrateTenant(cmd.tenant_id);
migrateOpers.push(oper);
}
Promise.all(migrateOpers).then(() => {
success('All tenants are rollbacked.');
});
} catch (error) {
exit(error);
}
});

View File

@@ -0,0 +1,4 @@
import commander from 'commander';
import './bigcapital';
commander.parse();

View File

@@ -9,7 +9,7 @@ if (envFound.error) {
throw new Error("⚠️ Couldn't find .env file ⚠️"); throw new Error("⚠️ Couldn't find .env file ⚠️");
} }
export default { module.exports = {
/** /**
* Your favorite port * Your favorite port
*/ */

View File

@@ -3,7 +3,7 @@ import TenancyService from '@/services/Tenancy/TenancyService';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { IAccountDTO, IAccount, IAccountCreateDTO } from '@/interfaces'; import { IAccountDTO, IAccount, IAccountCreateDTO } from '@/interfaces';
import AccountTypesUtils from '@/lib/AccountTypes'; import AccountTypesUtils from '@/lib/AccountTypes';
import { ERRORS } from './constants'; import { ERRORS, MAX_ACCOUNTS_CHART_DEPTH } from './constants';
@Service() @Service()
export class CommandAccountValidators { export class CommandAccountValidators {
@@ -160,7 +160,7 @@ export class CommandAccountValidators {
public validateCurrentSameParentAccount = ( public validateCurrentSameParentAccount = (
accountDTO: IAccountCreateDTO, accountDTO: IAccountCreateDTO,
parentAccount: IAccount, parentAccount: IAccount,
baseCurrency: string, baseCurrency: string
) => { ) => {
// If the account DTO currency not assigned and the parent account has no base currency. // If the account DTO currency not assigned and the parent account has no base currency.
if ( if (
@@ -208,4 +208,24 @@ export class CommandAccountValidators {
} }
return account; return account;
} }
/**
* Validates the max depth level of accounts chart.
* @param {numebr} tenantId - Tenant id.
* @param {number} parentAccountId - Parent account id.
*/
public async validateMaxParentAccountDepthLevels(
tenantId: number,
parentAccountId: number
) {
const { accountRepository } = this.tenancy.repositories(tenantId);
const accountsGraph = await accountRepository.getDependencyGraph();
const parentDependantsIds = accountsGraph.dependantsOf(parentAccountId);
if (parentDependantsIds.length >= MAX_ACCOUNTS_CHART_DEPTH) {
throw new ServiceError(ERRORS.PARENT_ACCOUNT_EXCEEDED_THE_DEPTH_LEVEL);
}
}
} }

View File

@@ -70,6 +70,11 @@ export class CreateAccount {
parentAccount, parentAccount,
baseCurrency baseCurrency
); );
// Validates the max depth level of accounts chart.
await this.validator.validateMaxParentAccountDepthLevels(
tenantId,
accountDTO.parentAccountId
);
} }
// Validates the given account type supports the multi-currency. // Validates the given account type supports the multi-currency.
this.validator.validateAccountTypeSupportCurrency(accountDTO, baseCurrency); this.validator.validateAccountTypeSupportCurrency(accountDTO, baseCurrency);

View File

@@ -5,6 +5,7 @@ import TenancyService from '@/services/Tenancy/TenancyService';
import DynamicListingService from '@/services/DynamicListing/DynamicListService'; import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { AccountTransformer } from './AccountTransform'; import { AccountTransformer } from './AccountTransform';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { flatToNestedArray } from '@/utils';
@Service() @Service()
export class GetAccounts { export class GetAccounts {
@@ -53,11 +54,17 @@ export class GetAccounts {
builder.modify('inactiveMode', filter.inactiveMode); builder.modify('inactiveMode', filter.inactiveMode);
}); });
// Retrievs the formatted accounts collection. // Retrievs the formatted accounts collection.
const transformedAccounts = await this.transformer.transform( const preTransformedAccounts = await this.transformer.transform(
tenantId, tenantId,
accounts, accounts,
new AccountTransformer() new AccountTransformer()
); );
// Transform accounts to nested array.
const transformedAccounts = flatToNestedArray(preTransformedAccounts, {
id: 'id',
parentId: 'parentAccountId',
});
return { return {
accounts: transformedAccounts, accounts: transformedAccounts,
filterMeta: dynamicList.getResponseMeta(), filterMeta: dynamicList.getResponseMeta(),

View File

@@ -13,8 +13,12 @@ export const ERRORS = {
CLOSE_ACCOUNT_AND_TO_ACCOUNT_NOT_SAME_TYPE: CLOSE_ACCOUNT_AND_TO_ACCOUNT_NOT_SAME_TYPE:
'close_account_and_to_account_not_same_type', 'close_account_and_to_account_not_same_type',
ACCOUNTS_NOT_FOUND: 'accounts_not_found', ACCOUNTS_NOT_FOUND: 'accounts_not_found',
ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY: 'ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY', ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY:
ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT: 'ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT', 'ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY',
ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT:
'ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT',
PARENT_ACCOUNT_EXCEEDED_THE_DEPTH_LEVEL:
'PARENT_ACCOUNT_EXCEEDED_THE_DEPTH_LEVEL',
}; };
// Default views columns. // Default views columns.
@@ -27,6 +31,8 @@ export const DEFAULT_VIEW_COLUMNS = [
{ key: 'currencyCode', label: 'Currency' }, { key: 'currencyCode', label: 'Currency' },
]; ];
export const MAX_ACCOUNTS_CHART_DEPTH = 5;
// Accounts default views. // Accounts default views.
export const DEFAULT_VIEWS = [ export const DEFAULT_VIEWS = [
{ {
@@ -43,7 +49,12 @@ export const DEFAULT_VIEWS = [
slug: 'liabilities', slug: 'liabilities',
rolesLogicExpression: '1', rolesLogicExpression: '1',
roles: [ roles: [
{ fieldKey: 'root_type', index: 1, comparator: 'equals', value: 'liability' }, {
fieldKey: 'root_type',
index: 1,
comparator: 'equals',
value: 'liability',
},
], ],
columns: DEFAULT_VIEW_COLUMNS, columns: DEFAULT_VIEW_COLUMNS,
}, },
@@ -52,7 +63,12 @@ export const DEFAULT_VIEWS = [
slug: 'equity', slug: 'equity',
rolesLogicExpression: '1', rolesLogicExpression: '1',
roles: [ roles: [
{ fieldKey: 'root_type', index: 1, comparator: 'equals', value: 'equity' }, {
fieldKey: 'root_type',
index: 1,
comparator: 'equals',
value: 'equity',
},
], ],
columns: DEFAULT_VIEW_COLUMNS, columns: DEFAULT_VIEW_COLUMNS,
}, },
@@ -61,7 +77,12 @@ export const DEFAULT_VIEWS = [
slug: 'income', slug: 'income',
rolesLogicExpression: '1', rolesLogicExpression: '1',
roles: [ roles: [
{ fieldKey: 'root_type', index: 1, comparator: 'equals', value: 'income' }, {
fieldKey: 'root_type',
index: 1,
comparator: 'equals',
value: 'income',
},
], ],
columns: DEFAULT_VIEW_COLUMNS, columns: DEFAULT_VIEW_COLUMNS,
}, },
@@ -70,7 +91,12 @@ export const DEFAULT_VIEWS = [
slug: 'expenses', slug: 'expenses',
rolesLogicExpression: '1', rolesLogicExpression: '1',
roles: [ roles: [
{ fieldKey: 'root_type', index: 1, comparator: 'equals', value: 'expense' }, {
fieldKey: 'root_type',
index: 1,
comparator: 'equals',
value: 'expense',
},
], ],
columns: DEFAULT_VIEW_COLUMNS, columns: DEFAULT_VIEW_COLUMNS,
}, },

View File

@@ -4,17 +4,20 @@ USER root
WORKDIR /app WORKDIR /app
COPY ./package.json /app/package.json # Install dependencies
COPY ./package-lock.json /app/package-lock.json COPY package.json ./
COPY lerna.json ./
COPY ./packages/webapp/package.json /app/packages/webapp/package.json
RUN npm install RUN npm install
RUN npm run bootstrap
COPY . . # Build webapp package
COPY ./packages/webapp /app/packages/webapp
RUN npm run build RUN npm run build:webapp
FROM nginx FROM nginx
COPY ./nginx/sites/default.conf /etc/nginx/conf.d/default.conf 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/build /usr/share/nginx/html

View File

@@ -1144,6 +1144,11 @@
"@babel/plugin-transform-typescript": "^7.9.0" "@babel/plugin-transform-typescript": "^7.9.0"
} }
}, },
"@babel/regjsgen": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz",
"integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA=="
},
"@babel/runtime": { "@babel/runtime": {
"version": "7.20.13", "version": "7.20.13",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz",
@@ -3516,9 +3521,9 @@
} }
}, },
"async-each": { "async-each": {
"version": "1.0.5", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.5.tgz", "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.6.tgz",
"integrity": "sha512-5QzqtU3BlagehwmdoqwaS2FBQF2P5eL6vFqXwNsb5jwoEsmtfAXg1ocFvW7I6/gGLFhBMKwcMwZuy7uv/Bo9jA==" "integrity": "sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg=="
}, },
"async-foreach": { "async-foreach": {
"version": "0.1.3", "version": "0.1.3",
@@ -4681,9 +4686,9 @@
} }
}, },
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30001450", "version": "1.0.30001451",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001450.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001451.tgz",
"integrity": "sha512-qMBmvmQmFXaSxexkjjfMvD5rnDL0+m+dUMZKoDYsGG8iZN29RuYh9eRoMvKsT6uMAWlyUUGDEQGJJYjzCIO9ew==" "integrity": "sha512-XY7UbUpGRatZzoRft//5xOa69/1iGJRBlrieH6QYrkKLIFn3m7OVEJ81dSrKoy2BnKsdbX5cLrOispZNYo9v2w=="
}, },
"capture-exit": { "capture-exit": {
"version": "2.0.0", "version": "2.0.0",
@@ -6015,9 +6020,9 @@
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
}, },
"electron-to-chromium": { "electron-to-chromium": {
"version": "1.4.285", "version": "1.4.289",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.285.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.289.tgz",
"integrity": "sha512-47o4PPgxfU1KMNejz+Dgaodf7YTcg48uOfV1oM6cs3adrl2+7R+dHkt3Jpxqo0LRCbGJEzTKMUt0RdvByb/leg==" "integrity": "sha512-relLdMfPBxqGCxy7Gyfm1HcbRPcFUJdlgnCPVgQ23sr1TvUrRJz0/QPoGP0+x41wOVSTN/Wi3w6YDgHiHJGOzg=="
}, },
"elliptic": { "elliptic": {
"version": "6.5.4", "version": "6.5.4",
@@ -10984,9 +10989,9 @@
} }
}, },
"node-releases": { "node-releases": {
"version": "2.0.9", "version": "2.0.10",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.9.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz",
"integrity": "sha512-2xfmOrRkGogbTK9R6Leda0DGiXeY3p2NJpy4+gNCffdUvV6mdEJnaDEic1i3Ec2djAo8jWYoJMR5PB0MSMpxUA==" "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w=="
}, },
"node-sass": { "node-sass": {
"version": "4.14.1", "version": "4.14.1",
@@ -14129,23 +14134,18 @@
"integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==" "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg=="
}, },
"regexpu-core": { "regexpu-core": {
"version": "5.2.2", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.2.tgz", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.0.tgz",
"integrity": "sha512-T0+1Zp2wjF/juXMrMxHxidqGYn8U4R+zleSJhX9tQ1PUsS8a9UtYfbsF9LdiVgNX3kiX8RNaKM42nfSgvFJjmw==", "integrity": "sha512-ZdhUQlng0RoscyW7jADnUZ25F5eVtHdMyXSb2PiwafvteRAOJUjFoUPEYZSIfP99fBIs3maLIRfpEddT78wAAQ==",
"requires": { "requires": {
"@babel/regjsgen": "^0.8.0",
"regenerate": "^1.4.2", "regenerate": "^1.4.2",
"regenerate-unicode-properties": "^10.1.0", "regenerate-unicode-properties": "^10.1.0",
"regjsgen": "^0.7.1",
"regjsparser": "^0.9.1", "regjsparser": "^0.9.1",
"unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-ecmascript": "^2.0.0",
"unicode-match-property-value-ecmascript": "^2.1.0" "unicode-match-property-value-ecmascript": "^2.1.0"
} }
}, },
"regjsgen": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.7.1.tgz",
"integrity": "sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA=="
},
"regjsparser": { "regjsparser": {
"version": "0.9.1", "version": "0.9.1",
"resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz",

View File

@@ -99,7 +99,7 @@
"yup": "^0.28.1" "yup": "^0.28.1"
}, },
"scripts": { "scripts": {
"dev": "craco start", "dev": "PORT=4000 craco start",
"build": "craco build", "build": "craco build",
"test": "node scripts/test.js", "test": "node scripts/test.js",
"storybook": "start-storybook -p 6006" "storybook": "start-storybook -p 6006"

View File

@@ -72,6 +72,7 @@ export default function TableCell({ cell, row, index }) {
[`td-${cell.column.id}`]: cell.column.id, [`td-${cell.column.id}`]: cell.column.id,
[`td-${cellType}-type`]: !!cellType, [`td-${cellType}-type`]: !!cellType,
}), }),
tabindex: 0,
onClick: handleCellClick, onClick: handleCellClick,
})} })}
> >

View File

@@ -3,7 +3,6 @@ import React, { useCallback } from 'react';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import { Intent } from '@blueprintjs/core'; import { Intent } from '@blueprintjs/core';
import { Formik } from 'formik'; import { Formik } from 'formik';
import { omit } from 'lodash';
import { AppToaster } from '@/components'; import { AppToaster } from '@/components';
import AccountDialogFormContent from './AccountDialogFormContent'; import AccountDialogFormContent from './AccountDialogFormContent';
@@ -14,7 +13,11 @@ import {
CreateAccountFormSchema, CreateAccountFormSchema,
} from './AccountForm.schema'; } from './AccountForm.schema';
import { compose, transformToForm } from '@/utils'; import { compose, transformToForm } from '@/utils';
import { transformApiErrors, transformAccountToForm } from './utils'; import {
transformApiErrors,
transformAccountToForm,
transformFormToReq,
} from './utils';
import '@/style/pages/Accounts/AccountFormDialog.scss'; import '@/style/pages/Accounts/AccountFormDialog.scss';
import { useAccountDialogContext } from './AccountDialogProvider'; import { useAccountDialogContext } from './AccountDialogProvider';
@@ -26,7 +29,7 @@ const defaultInitialValues = {
name: '', name: '',
code: '', code: '',
description: '', description: '',
currency_code:'', currency_code: '',
subaccount: false, subaccount: false,
}; };
@@ -43,7 +46,6 @@ function AccountFormDialogContent({
createAccountMutate, createAccountMutate,
account, account,
accountId,
payload, payload,
isNewMode, isNewMode,
dialogName, dialogName,
@@ -56,7 +58,7 @@ function AccountFormDialogContent({
// Callbacks handles form submit. // Callbacks handles form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => { const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
const form = omit(values, ['subaccount']); const form = transformFormToReq(values);
const toastAccountName = values.code const toastAccountName = values.code
? `${values.code} - ${values.name}` ? `${values.code} - ${values.name}`
: values.name; : values.name;
@@ -90,8 +92,8 @@ function AccountFormDialogContent({
setErrors({ ...errorsTransformed }); setErrors({ ...errorsTransformed });
setSubmitting(false); setSubmitting(false);
}; };
if (accountId) { if (payload.accountId) {
editAccountMutate([accountId, form]) editAccountMutate([payload.accountId, form])
.then(handleSuccess) .then(handleSuccess)
.catch(handleError); .catch(handleError);
} else { } else {
@@ -113,7 +115,6 @@ function AccountFormDialogContent({
defaultInitialValues, defaultInitialValues,
), ),
}; };
// Handles dialog close. // Handles dialog close.
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
closeDialog(dialogName); closeDialog(dialogName);

View File

@@ -26,6 +26,7 @@ import { inputIntent, compose } from '@/utils';
import { useAutofocus } from '@/hooks'; import { useAutofocus } from '@/hooks';
import { FOREIGN_CURRENCY_ACCOUNTS } from '@/constants/accountTypes'; import { FOREIGN_CURRENCY_ACCOUNTS } from '@/constants/accountTypes';
import { useAccountDialogContext } from './AccountDialogProvider'; import { useAccountDialogContext } from './AccountDialogProvider';
import { parentAccountShouldUpdate } from './utils';
/** /**
* Account form dialogs fields. * Account form dialogs fields.
@@ -115,12 +116,7 @@ function AccountFormDialogFields({
> >
<Checkbox <Checkbox
inline={true} inline={true}
label={ label={<T id={'sub_account'} />}
<>
<T id={'sub_account'} />
<Hint />
</>
}
name={'subaccount'} name={'subaccount'}
{...field} {...field}
/> />
@@ -128,37 +124,36 @@ function AccountFormDialogFields({
)} )}
</Field> </Field>
<If condition={values.subaccount}> <FastField
<FastField name={'parent_account_id'}> name={'parent_account_id'}
{({ shouldUpdate={parentAccountShouldUpdate}
form: { values, setFieldValue }, >
field: { value }, {({
meta: { error, touched }, form: { values, setFieldValue },
}) => ( field: { value },
<FormGroup meta: { error, touched },
label={<T id={'parent_account'} />} }) => (
className={classNames( <FormGroup
'form-group--parent-account', label={<T id={'parent_account'} />}
Classes.FILL, className={classNames('form-group--parent-account', Classes.FILL)}
)} inline={true}
inline={true} intent={inputIntent({ error, touched })}
intent={inputIntent({ error, touched })} helperText={<ErrorMessage name="parent_account_id" />}
helperText={<ErrorMessage name="parent_account_id" />} >
> <AccountsSelectList
<AccountsSelectList accounts={accounts}
accounts={accounts} onAccountSelected={(account) => {
onAccountSelected={(account) => { setFieldValue('parent_account_id', account.id);
setFieldValue('parent_account_id', account.id); }}
}} defaultSelectText={<T id={'select_parent_account'} />}
defaultSelectText={<T id={'select_parent_account'} />} selectedAccountId={value}
selectedAccountId={value} popoverFill={true}
popoverFill={true} filterByTypes={values.account_type}
filterByTypes={values.account_type} disabled={!values.subaccount}
/> />
</FormGroup> </FormGroup>
)} )}
</FastField> </FastField>
</If>
<If condition={FOREIGN_CURRENCY_ACCOUNTS.includes(values.account_type)}> <If condition={FOREIGN_CURRENCY_ACCOUNTS.includes(values.account_type)}>
{/*------------ Currency -----------*/} {/*------------ Currency -----------*/}

View File

@@ -2,6 +2,7 @@
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import * as R from 'ramda'; import * as R from 'ramda';
import { isUndefined } from 'lodash'; import { isUndefined } from 'lodash';
import { defaultFastFieldShouldUpdate } from '@/utils';
export const AccountDialogAction = { export const AccountDialogAction = {
Edit: 'edit', Edit: 'edit',
@@ -33,7 +34,7 @@ export const transformApiErrors = (errors) => {
/** /**
* Payload transformer in account edit mode. * Payload transformer in account edit mode.
*/ */
function tranformNewChildAccountPayload(payload) { function tranformNewChildAccountPayload(account, payload) {
return { return {
parent_account_id: payload.parentAccountId || '', parent_account_id: payload.parentAccountId || '',
account_type: payload.accountType || '', account_type: payload.accountType || '',
@@ -44,7 +45,7 @@ function tranformNewChildAccountPayload(payload) {
/** /**
* Payload transformer in new account with defined type. * Payload transformer in new account with defined type.
*/ */
function transformNewDefinedTypePayload(payload) { function transformNewDefinedTypePayload(account, payload) {
return { return {
account_type: payload.accountType || '', account_type: payload.accountType || '',
}; };
@@ -63,7 +64,9 @@ const mergeWithAccount = R.curry((transformed, account) => {
/** /**
* Default account payload transformer. * Default account payload transformer.
*/ */
const defaultPayloadTransform = () => ({}); const defaultPayloadTransform = (account, payload) => ({
subaccount: !!account.parent_account_id,
});
/** /**
* Defined payload transformers. * Defined payload transformers.
@@ -89,7 +92,7 @@ export const transformAccountToForm = (account, payload) => {
return [ return [
condition[0] === payload.action ? R.T : R.F, condition[0] === payload.action ? R.T : R.F,
mergeWithAccount(transformer(payload)), mergeWithAccount(transformer(account, payload)),
]; ];
}); });
return R.cond(results)(account); return R.cond(results)(account);
@@ -106,3 +109,29 @@ export const getDisabledFormFields = (account, payload) => {
payload.action === AccountDialogAction.NewDefinedType, payload.action === AccountDialogAction.NewDefinedType,
}; };
}; };
/**
* Detarmines whether should update the parent account field.
* @param newProps
* @param oldProps
* @returns {boolean}
*/
export const parentAccountShouldUpdate = (newProps, oldProps) => {
return (
newProps.formik.values.subaccount !== oldProps.formik.values.subaccount ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};
/**
* Transformes the form values to the request.
*/
export const transformFormToReq = (form) => {
return R.compose(
R.omit(['subaccount']),
R.when(
R.propSatisfies(R.equals(R.__, false), 'subaccount'),
R.assoc(['parent_account_id'], ''),
),
)(form);
};

View File

@@ -105,7 +105,7 @@ export function ItemsActionMenuList({
</Can> </Can>
<Can I={ItemAction.Create} a={AbilitySubject.Item}> <Can I={ItemAction.Create} a={AbilitySubject.Item}>
<MenuItem <MenuItem
icon={<Icon icon="duplicate-16" />} icon={<Icon icon="content-copy" iconSize={16} />}
text={intl.get('duplicate')} text={intl.get('duplicate')}
onClick={safeCallback(onDuplicate, original)} onClick={safeCallback(onDuplicate, original)}
/> />

View File

@@ -559,4 +559,10 @@ export default {
], ],
viewBox: '0 0 24 24', viewBox: '0 0 24 24',
}, },
'content-copy': {
path: [
'M15 0H5c-.55 0-1 .45-1 1v2h2V2h8v7h-1v2h2c.55 0 1-.45 1-1V1c0-.55-.45-1-1-1zm-4 4H1c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h10c.55 0 1-.45 1-1V5c0-.55-.45-1-1-1zm-1 10H2V6h8v8z'
],
viewBox: '0 0 16 16'
}
}; };

View File

@@ -10,7 +10,7 @@
display: block; display: block;
.thead .thead-inner, .thead .thead-inner,
.tbody .tbody-inner{ .tbody .tbody-inner {
min-width: fit-content; min-width: fit-content;
} }
@@ -25,7 +25,7 @@
font-weight: 400; font-weight: 400;
border-bottom: 1px solid #d2dde2; border-bottom: 1px solid #d2dde2;
>div { > div {
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -208,6 +208,10 @@
width: 100%; width: 100%;
} }
} }
&:focus {
outline: 1px solid rgba(0, 82, 204, 0.7);
outline-offset: -1px;
}
} }
.tr:hover .td { .tr:hover .td {
@@ -357,13 +361,9 @@
position: sticky; position: sticky;
} }
[data-sticky-last-left-td] { [data-sticky-last-left-td] {}
} [data-sticky-first-right-td] {}
[data-sticky-first-right-td] {
}
} }
&.has-virtualized-rows { &.has-virtualized-rows {

9
vercel.json Normal file
View File

@@ -0,0 +1,9 @@
{
"buildCommand": "npm run build:webapp",
"devCommand": "npm run dev:webapp",
"installCommand": "npm install && npm run bootstrap",
"outputDirectory": "packages/webapp/build",
"env": {
"CI": "false"
}
}