mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 05:40:31 +00:00
Compare commits
22 Commits
vercel
...
vercel-ign
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a142b734d3 | ||
|
|
2263cf5657 | ||
|
|
e488c0eea9 | ||
|
|
f093239a15 | ||
|
|
5c537e094d | ||
|
|
a371fd44f7 | ||
|
|
59cb168331 | ||
|
|
8a5fbfc041 | ||
|
|
e3a072e267 | ||
|
|
b03606406e | ||
|
|
a1a7ee2b5b | ||
|
|
228ae71a1c | ||
|
|
71a8d3e77f | ||
|
|
4ddeb927cc | ||
|
|
72c1685fa6 | ||
|
|
7e7ee24109 | ||
|
|
708d971717 | ||
|
|
7781d092ca | ||
|
|
d0e84fb51a | ||
|
|
0e673ffa7c | ||
|
|
4c4c73db2d | ||
|
|
0086ee5186 |
5
.github/workflows/docker-build.yml
vendored
5
.github/workflows/docker-build.yml
vendored
@@ -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
5
.vercelignore
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/*
|
||||||
|
!package.json
|
||||||
|
!package-lock.json
|
||||||
|
!yarn.lock
|
||||||
|
!packages/webapp
|
||||||
5697
package-lock.json
generated
Normal file
5697
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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": [
|
||||||
|
|||||||
@@ -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>",
|
||||||
|
|||||||
11
packages/server/scripts/webpack.cli.js
Normal file
11
packages/server/scripts/webpack.cli.js
Normal 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,
|
||||||
|
});
|
||||||
76
packages/server/scripts/webpack.common.js
Normal file
76
packages/server/scripts/webpack.common.js
Normal 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;
|
||||||
|
};
|
||||||
@@ -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;
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
282
packages/server/src/commands/bigcapital.ts
Normal file
282
packages/server/src/commands/bigcapital.ts
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
4
packages/server/src/commands/index.ts
Normal file
4
packages/server/src/commands/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import commander from 'commander';
|
||||||
|
import './bigcapital';
|
||||||
|
|
||||||
|
commander.parse();
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -154,13 +154,13 @@ export class CommandAccountValidators {
|
|||||||
* parent account.
|
* parent account.
|
||||||
* @param {IAccountCreateDTO} accountDTO
|
* @param {IAccountCreateDTO} accountDTO
|
||||||
* @param {IAccount} parentAccount
|
* @param {IAccount} parentAccount
|
||||||
* @param {string} baseCurrency -
|
* @param {string} baseCurrency -
|
||||||
* @throws {ServiceError(ERRORS.ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT)}
|
* @throws {ServiceError(ERRORS.ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT)}
|
||||||
*/
|
*/
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
42
packages/webapp/package-lock.json
generated
42
packages/webapp/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 -----------*/}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
};
|
||||||
|
|||||||
@@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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'
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
9
vercel.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user