chore: Refactoring all import directories to alias and all .js|.jsx renamed to be .ts|.tsx

This commit is contained in:
a.bouhuolia
2022-07-15 23:25:23 +02:00
parent cd08d0ee16
commit f00097f6c8
3846 changed files with 125197 additions and 128236 deletions

View File

@@ -1 +1,3 @@
APP_VERSION=$npm_package_version
REACT_APP_VERSION=$npm_package_version
TSC_COMPILE_ON_ERROR=true
ESLINT_NO_DEV_ERRORS=true

13
.storybook/main.js Normal file
View File

@@ -0,0 +1,13 @@
module.exports = {
"stories": [
"../src/**/*.stories.mdx",
"../src/**/*.stories.@(js|jsx|ts|tsx)"
],
"addons": [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
"@storybook/preset-create-react-app"
],
"framework": "@storybook/react"
}

9
.storybook/preview.js Normal file
View File

@@ -0,0 +1,9 @@
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
}

View File

@@ -1,104 +0,0 @@
'use strict';
const fs = require('fs');
const path = require('path');
const paths = require('./paths');
// Make sure that including paths.js after env.js will read .env variables.
delete require.cache[require.resolve('./paths')];
const NODE_ENV = process.env.NODE_ENV;
if (!NODE_ENV) {
throw new Error(
'The NODE_ENV environment variable is required but was not specified.'
);
}
// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
const dotenvFiles = [
`${paths.dotenv}.${NODE_ENV}.local`,
`${paths.dotenv}.${NODE_ENV}`,
// Don't include `.env.local` for `test` environment
// since normally you expect tests to produce the same
// results for everyone
NODE_ENV !== 'test' && `${paths.dotenv}.local`,
paths.dotenv,
].filter(Boolean);
// Load environment variables from .env* files. Suppress warnings using silent
// if this file is missing. dotenv will never modify any environment variables
// that have already been set. Variable expansion is supported in .env files.
// https://github.com/motdotla/dotenv
// https://github.com/motdotla/dotenv-expand
dotenvFiles.forEach(dotenvFile => {
if (fs.existsSync(dotenvFile)) {
require('dotenv-expand')(
require('dotenv').config({
path: dotenvFile,
})
);
}
});
// We support resolving modules according to `NODE_PATH`.
// This lets you use absolute paths in imports inside large monorepos:
// https://github.com/facebook/create-react-app/issues/253.
// It works similar to `NODE_PATH` in Node itself:
// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored.
// Otherwise, we risk importing Node.js core modules into an app instead of webpack shims.
// https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421
// We also resolve them to make sure all tools using them work consistently.
const appDirectory = fs.realpathSync(process.cwd());
process.env.NODE_PATH = (process.env.NODE_PATH || '')
.split(path.delimiter)
.filter(folder => folder && !path.isAbsolute(folder))
.map(folder => path.resolve(appDirectory, folder))
.join(path.delimiter);
// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
// injected into the application via DefinePlugin in webpack configuration.
const REACT_APP = /^REACT_APP_/i;
function getClientEnvironment(publicUrl) {
const raw = Object.keys(process.env)
.filter(key => REACT_APP.test(key))
.reduce(
(env, key) => {
env[key] = process.env[key];
return env;
},
{
// Useful for determining whether were running in production mode.
// Most importantly, it switches React into the correct mode.
NODE_ENV: process.env.NODE_ENV || 'development',
// Useful for resolving the correct path to static assets in `public`.
// For example, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />.
// This should only be used as an escape hatch. Normally you would put
// images into the `src` and `import` them in code to get their paths.
PUBLIC_URL: publicUrl,
// We support configuring the sockjs pathname during development.
// These settings let a developer run multiple simultaneous projects.
// They are used as the connection `hostname`, `pathname` and `port`
// in webpackHotDevClient. They are used as the `sockHost`, `sockPath`
// and `sockPort` options in webpack-dev-server.
WDS_SOCKET_HOST: process.env.WDS_SOCKET_HOST,
WDS_SOCKET_PATH: process.env.WDS_SOCKET_PATH,
WDS_SOCKET_PORT: process.env.WDS_SOCKET_PORT,
// Application version.
VERSION: paths.appVersion
}
);
// Stringify all values so we can feed into webpack DefinePlugin
const stringified = {
'process.env': Object.keys(raw).reduce((env, key) => {
env[key] = JSON.stringify(raw[key]);
return env;
}, {}),
};
return { raw, stringified };
}
module.exports = getClientEnvironment;

View File

@@ -1,66 +0,0 @@
'use strict';
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const chalk = require('react-dev-utils/chalk');
const paths = require('./paths');
// Ensure the certificate and key provided are valid and if not
// throw an easy to debug error
function validateKeyAndCerts({ cert, key, keyFile, crtFile }) {
let encrypted;
try {
// publicEncrypt will throw an error with an invalid cert
encrypted = crypto.publicEncrypt(cert, Buffer.from('test'));
} catch (err) {
throw new Error(
`The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}`
);
}
try {
// privateDecrypt will throw an error with an invalid key
crypto.privateDecrypt(key, encrypted);
} catch (err) {
throw new Error(
`The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${
err.message
}`
);
}
}
// Read file and throw an error if it doesn't exist
function readEnvFile(file, type) {
if (!fs.existsSync(file)) {
throw new Error(
`You specified ${chalk.cyan(
type
)} in your env, but the file "${chalk.yellow(file)}" can't be found.`
);
}
return fs.readFileSync(file);
}
// Get the https config
// Return cert files if provided in env, otherwise just true or false
function getHttpsConfig() {
const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env;
const isHttps = HTTPS === 'true';
if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) {
const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE);
const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE);
const config = {
cert: readEnvFile(crtFile, 'SSL_CRT_FILE'),
key: readEnvFile(keyFile, 'SSL_KEY_FILE'),
};
validateKeyAndCerts({ ...config, keyFile, crtFile });
return config;
}
return isHttps;
}
module.exports = getHttpsConfig;

View File

@@ -1,14 +0,0 @@
'use strict';
// This is a custom Jest transformer turning style imports into empty objects.
// http://facebook.github.io/jest/docs/en/webpack.html
module.exports = {
process() {
return 'module.exports = {};';
},
getCacheKey() {
// The output is always the same.
return 'cssTransform';
},
};

View File

@@ -1,40 +0,0 @@
'use strict';
const path = require('path');
const camelcase = require('camelcase');
// This is a custom Jest transformer turning file imports into filenames.
// http://facebook.github.io/jest/docs/en/webpack.html
module.exports = {
process(src, filename) {
const assetFilename = JSON.stringify(path.basename(filename));
if (filename.match(/\.svg$/)) {
// Based on how SVGR generates a component name:
// https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6
const pascalCaseFilename = camelcase(path.parse(filename).name, {
pascalCase: true,
});
const componentName = `Svg${pascalCaseFilename}`;
return `const React = require('react');
module.exports = {
__esModule: true,
default: ${assetFilename},
ReactComponent: React.forwardRef(function ${componentName}(props, ref) {
return {
$$typeof: Symbol.for('react.element'),
type: 'svg',
ref: ref,
key: null,
props: Object.assign({}, props, {
children: ${assetFilename}
})
};
}),
};`;
}
return `module.exports = ${assetFilename};`;
},
};

View File

@@ -1,141 +0,0 @@
'use strict';
const fs = require('fs');
const path = require('path');
const paths = require('./paths');
const chalk = require('react-dev-utils/chalk');
const resolve = require('resolve');
/**
* Get additional module paths based on the baseUrl of a compilerOptions object.
*
* @param {Object} options
*/
function getAdditionalModulePaths(options = {}) {
const baseUrl = options.baseUrl;
// We need to explicitly check for null and undefined (and not a falsy value) because
// TypeScript treats an empty string as `.`.
if (baseUrl == null) {
// If there's no baseUrl set we respect NODE_PATH
// Note that NODE_PATH is deprecated and will be removed
// in the next major release of create-react-app.
const nodePath = process.env.NODE_PATH || '';
return nodePath.split(path.delimiter).filter(Boolean);
}
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
// We don't need to do anything if `baseUrl` is set to `node_modules`. This is
// the default behavior.
if (path.relative(paths.appNodeModules, baseUrlResolved) === '') {
return null;
}
// Allow the user set the `baseUrl` to `appSrc`.
if (path.relative(paths.appSrc, baseUrlResolved) === '') {
return [paths.appSrc];
}
// If the path is equal to the root directory we ignore it here.
// We don't want to allow importing from the root directly as source files are
// not transpiled outside of `src`. We do allow importing them with the
// absolute path (e.g. `src/Components/Button.js`) but we set that up with
// an alias.
if (path.relative(paths.appPath, baseUrlResolved) === '') {
return null;
}
// Otherwise, throw an error.
throw new Error(
chalk.red.bold(
"Your project's `baseUrl` can only be set to `src` or `node_modules`." +
' Create React App does not support other values at this time.'
)
);
}
/**
* Get webpack aliases based on the baseUrl of a compilerOptions object.
*
* @param {*} options
*/
function getWebpackAliases(options = {}) {
const baseUrl = options.baseUrl;
if (!baseUrl) {
return {};
}
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
if (path.relative(paths.appPath, baseUrlResolved) === '') {
return {
src: paths.appSrc,
};
}
}
/**
* Get jest aliases based on the baseUrl of a compilerOptions object.
*
* @param {*} options
*/
function getJestAliases(options = {}) {
const baseUrl = options.baseUrl;
if (!baseUrl) {
return {};
}
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
if (path.relative(paths.appPath, baseUrlResolved) === '') {
return {
'^src/(.*)$': '<rootDir>/src/$1',
};
}
}
function getModules() {
// Check if TypeScript is setup
const hasTsConfig = fs.existsSync(paths.appTsConfig);
const hasJsConfig = fs.existsSync(paths.appJsConfig);
if (hasTsConfig && hasJsConfig) {
throw new Error(
'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.'
);
}
let config;
// If there's a tsconfig.json we assume it's a
// TypeScript project and set up the config
// based on tsconfig.json
if (hasTsConfig) {
const ts = require(resolve.sync('typescript', {
basedir: paths.appNodeModules,
}));
config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config;
// Otherwise we'll check if there is jsconfig.json
// for non TS projects.
} else if (hasJsConfig) {
config = require(paths.appJsConfig);
}
config = config || {};
const options = config.compilerOptions || {};
const additionalModulePaths = getAdditionalModulePaths(options);
return {
additionalModulePaths: additionalModulePaths,
webpackAliases: getWebpackAliases(options),
jestAliases: getJestAliases(options),
hasTsConfig,
};
}
module.exports = getModules();

View File

@@ -1,75 +0,0 @@
'use strict';
const path = require('path');
const fs = require('fs');
const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath');
// Make sure any symlinks in the project folder are resolved:
// https://github.com/facebook/create-react-app/issues/637
const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
// We use `PUBLIC_URL` environment variable or "homepage" field to infer
// "public path" at which the app is served.
// webpack needs to know it to put the right <script> hrefs into HTML even in
// single-page apps that may serve index.html for nested URLs like /todos/42.
// We can't use a relative path in HTML because we don't want to load something
// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
const publicUrlOrPath = getPublicUrlOrPath(
process.env.NODE_ENV === 'development',
require(resolveApp('package.json')).homepage,
process.env.PUBLIC_URL
);
const moduleFileExtensions = [
'web.mjs',
'mjs',
'web.js',
'js',
'web.ts',
'ts',
'web.tsx',
'tsx',
'json',
'web.jsx',
'jsx',
];
// Resolve file paths in the same order as webpack
const resolveModule = (resolveFn, filePath) => {
const extension = moduleFileExtensions.find(extension =>
fs.existsSync(resolveFn(`${filePath}.${extension}`))
);
if (extension) {
return resolveFn(`${filePath}.${extension}`);
}
return resolveFn(`${filePath}.js`);
};
const appVersion = require(resolveApp('package.json')).version;
// config after eject: we're in ./config/
module.exports = {
dotenv: resolveApp('.env'),
appPath: resolveApp('.'),
appBuild: resolveApp('build'),
appPublic: resolveApp('public'),
appHtml: resolveApp('public/index.html'),
appIndexJs: resolveModule(resolveApp, 'src/index'),
appPackageJson: resolveApp('package.json'),
appSrc: resolveApp('src'),
appTsConfig: resolveApp('tsconfig.json'),
appJsConfig: resolveApp('jsconfig.json'),
yarnLockFile: resolveApp('yarn.lock'),
testsSetup: resolveModule(resolveApp, 'src/setupTests'),
proxySetup: resolveApp('src/setupProxy.js'),
appNodeModules: resolveApp('node_modules'),
publicUrlOrPath,
appVersion
};
module.exports.moduleFileExtensions = moduleFileExtensions;

View File

@@ -1,35 +0,0 @@
'use strict';
const { resolveModuleName } = require('ts-pnp');
exports.resolveModuleName = (
typescript,
moduleName,
containingFile,
compilerOptions,
resolutionHost
) => {
return resolveModuleName(
moduleName,
containingFile,
compilerOptions,
resolutionHost,
typescript.resolveModuleName
);
};
exports.resolveTypeReferenceDirective = (
typescript,
moduleName,
containingFile,
compilerOptions,
resolutionHost
) => {
return resolveModuleName(
moduleName,
containingFile,
compilerOptions,
resolutionHost,
typescript.resolveTypeReferenceDirective
);
};

View File

@@ -1,691 +0,0 @@
'use strict';
const fs = require('fs');
const path = require('path');
const webpack = require('webpack');
const resolve = require('resolve');
const PnpWebpackPlugin = require('pnp-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
const TerserPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const safePostCssParser = require('postcss-safe-parser');
const ManifestPlugin = require('webpack-manifest-plugin');
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
const paths = require('./paths');
const modules = require('./modules');
const getClientEnvironment = require('./env');
const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
const ForkTsCheckerWebpackPlugin = require('react-dev-utils/ForkTsCheckerWebpackPlugin');
const typescriptFormatter = require('react-dev-utils/typescriptFormatter');
const CompressionPlugin = require("compression-webpack-plugin");
const postcssNormalize = require('postcss-normalize');
const { postcssRTLCSS} = require('postcss-rtlcss');
const appPackageJson = require(paths.appPackageJson);
// Source maps are resource heavy and can cause out of memory issue for large source files.
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
// Some apps do not need the benefits of saving a web request, so not inlining the chunk
// makes for a smoother build process.
const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';
const imageInlineSizeLimit = parseInt(
process.env.IMAGE_INLINE_SIZE_LIMIT || '10000'
);
// Check if TypeScript is setup
const useTypeScript = fs.existsSync(paths.appTsConfig);
// style files regexes
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;
// This is the production and development configuration.
// It is focused on developer experience, fast rebuilds, and a minimal bundle.
module.exports = function(webpackEnv) {
const isEnvDevelopment = webpackEnv === 'development';
const isEnvProduction = webpackEnv === 'production';
// Variable used for enabling profiling in Production
// passed into alias object. Uses a flag if passed into the build command
const isEnvProductionProfile =
isEnvProduction && process.argv.includes('--profile');
// We will provide `paths.publicUrlOrPath` to our app
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
// Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
// Get environment variables to inject into our app.
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
// common function to get style loaders
const getStyleLoaders = (cssOptions, preProcessor) => {
const loaders = [
isEnvDevelopment && require.resolve('style-loader'),
isEnvProduction && {
loader: MiniCssExtractPlugin.loader,
// css is located in `static/css`, use '../../' to locate index.html folder
// in production `paths.publicUrlOrPath` can be a relative path
options: paths.publicUrlOrPath.startsWith('.')
? { publicPath: '../../' }
: {},
},
{
loader: require.resolve('css-loader'),
options: cssOptions,
},
{
// Options for PostCSS as we reference these options twice
// Adds vendor prefixing based on your specified browser support in
// package.json
loader: require.resolve('postcss-loader'),
options: {
// Necessary for external CSS imports to work
// https://github.com/facebook/create-react-app/issues/2677
ident: 'postcss',
plugins: () => [
// Postcss rtlcss plugin.
// require( 'postcss-rtl' )({
// // options here.
// removeComments: false,
// }),
postcssRTLCSS({
}),
require('postcss-flexbugs-fixes'),
require('postcss-preset-env')({
autoprefixer: {
flexbox: 'no-2009',
},
stage: 3,
}),
// Adds PostCSS Normalize as the reset css with default options,
// so that it honors browserslist config in package.json
// which in turn let's users customize the target behavior as per their needs.
postcssNormalize(),
],
sourceMap: isEnvProduction && shouldUseSourceMap,
},
},
].filter(Boolean);
if (preProcessor) {
loaders.push(
{
loader: require.resolve('resolve-url-loader'),
options: {
sourceMap: isEnvProduction && shouldUseSourceMap,
},
},
{
loader: require.resolve(preProcessor),
options: {
sourceMap: true,
sassOptions: {
sourceComments: true,
outputStyle: 'expanded'
}
},
}
);
}
return loaders;
};
return {
mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
// Stop compilation early in production
bail: isEnvProduction,
devtool: isEnvProduction
? shouldUseSourceMap
? 'source-map'
: false
: isEnvDevelopment && 'cheap-module-source-map',
// These are the "entry points" to our application.
// This means they will be the "root" imports that are included in JS bundle.
entry: [
// Include an alternative client for WebpackDevServer. A client's job is to
// connect to WebpackDevServer by a socket and get notified about changes.
// When you save a file, the client will either apply hot updates (in case
// of CSS changes), or refresh the page (in case of JS changes). When you
// make a syntax error, this client will display a syntax error overlay.
// Note: instead of the default WebpackDevServer client, we use a custom one
// to bring better experience for Create React App users. You can replace
// the line below with these two lines if you prefer the stock client:
// require.resolve('webpack-dev-server/client') + '?/',
// require.resolve('webpack/hot/dev-server'),
isEnvDevelopment &&
require.resolve('react-dev-utils/webpackHotDevClient'),
// Finally, this is your app's code:
paths.appIndexJs,
// We include the app code last so that if there is a runtime error during
// initialization, it doesn't blow up the WebpackDevServer client, and
// changing JS code would still trigger a refresh.
].filter(Boolean),
output: {
// The build folder.
path: isEnvProduction ? paths.appBuild : undefined,
// Add /* filename */ comments to generated require()s in the output.
pathinfo: isEnvDevelopment,
// There will be one main bundle, and one file per asynchronous chunk.
// In development, it does not produce real files.
filename: isEnvProduction
? 'static/js/[name].[contenthash:8].js'
: isEnvDevelopment && 'static/js/bundle.js',
// TODO: remove this when upgrading to webpack 5
futureEmitAssets: true,
// There are also additional JS chunk files if you use code splitting.
chunkFilename: isEnvProduction
? 'static/js/[name].[contenthash:8].chunk.js'
: isEnvDevelopment && 'static/js/[name].chunk.js',
// webpack uses `publicPath` to determine where the app is being served from.
// It requires a trailing slash, or the file assets will get an incorrect path.
// We inferred the "public path" (such as / or /my-project) from homepage.
publicPath: paths.publicUrlOrPath,
// Point sourcemap entries to original disk location (format as URL on Windows)
devtoolModuleFilenameTemplate: isEnvProduction
? info =>
path
.relative(paths.appSrc, info.absoluteResourcePath)
.replace(/\\/g, '/')
: isEnvDevelopment &&
(info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
// Prevents conflicts when multiple webpack runtimes (from different apps)
// are used on the same page.
jsonpFunction: `webpackJsonp${appPackageJson.name}`,
// this defaults to 'window', but by setting it to 'this' then
// module chunks which are built will work in web workers as well.
globalObject: 'this',
},
optimization: {
minimize: isEnvProduction,
minimizer: [
// This is only used in production mode
new TerserPlugin({
terserOptions: {
parse: {
// We want terser to parse ecma 8 code. However, we don't want it
// to apply any minification steps that turns valid ecma 5 code
// into invalid ecma 5 code. This is why the 'compress' and 'output'
// sections only apply transformations that are ecma 5 safe
// https://github.com/facebook/create-react-app/pull/4234
ecma: 8,
},
compress: {
ecma: 5,
warnings: false,
// Disabled because of an issue with Uglify breaking seemingly valid code:
// https://github.com/facebook/create-react-app/issues/2376
// Pending further investigation:
// https://github.com/mishoo/UglifyJS2/issues/2011
comparisons: false,
// Disabled because of an issue with Terser breaking valid code:
// https://github.com/facebook/create-react-app/issues/5250
// Pending further investigation:
// https://github.com/terser-js/terser/issues/120
inline: 2,
},
mangle: {
safari10: true,
},
// Added for profiling in devtools
keep_classnames: isEnvProductionProfile,
keep_fnames: isEnvProductionProfile,
output: {
ecma: 5,
comments: false,
// Turned on because emoji and regex is not minified properly using default
// https://github.com/facebook/create-react-app/issues/2488
ascii_only: true,
},
},
sourceMap: shouldUseSourceMap,
}),
// This is only used in production mode
new OptimizeCSSAssetsPlugin({
cssProcessorOptions: {
parser: safePostCssParser,
map: shouldUseSourceMap
? {
// `inline: false` forces the sourcemap to be output into a
// separate file
inline: false,
// `annotation: true` appends the sourceMappingURL to the end of
// the css file, helping the browser find the sourcemap
annotation: true,
}
: false,
},
cssProcessorPluginOptions: {
preset: ['default', { minifyFontValues: { removeQuotes: false } }],
},
}),
],
// Automatically split vendor and commons
// https://twitter.com/wSokra/status/969633336732905474
// https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366
splitChunks: {
chunks: 'all',
name: false,
},
// Keep the runtime chunk separated to enable long term caching
// https://twitter.com/wSokra/status/969679223278505985
// https://github.com/facebook/create-react-app/issues/5358
runtimeChunk: {
name: entrypoint => `runtime-${entrypoint.name}`,
},
},
resolve: {
// This allows you to set a fallback for where webpack should look for modules.
// We placed these paths second because we want `node_modules` to "win"
// if there are any conflicts. This matches Node resolution mechanism.
// https://github.com/facebook/create-react-app/issues/253
modules: ['node_modules', paths.appNodeModules].concat(
modules.additionalModulePaths || []
),
// These are the reasonable defaults supported by the Node ecosystem.
// We also include JSX as a common component filename extension to support
// some tools, although we do not recommend using it, see:
// https://github.com/facebook/create-react-app/issues/290
// `web` extension prefixes have been added for better support
// for React Native Web.
extensions: paths.moduleFileExtensions
.map(ext => `.${ext}`)
.filter(ext => useTypeScript || !ext.includes('ts')),
alias: {
// Support React Native Web
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
'react-native': 'react-native-web',
// Allows for better profiling with ReactDevTools
...(isEnvProductionProfile && {
'react-dom$': 'react-dom/profiling',
'scheduler/tracing': 'scheduler/tracing-profiling',
}),
...(modules.webpackAliases || {}),
},
plugins: [
// Adds support for installing with Plug'n'Play, leading to faster installs and adding
// guards against forgotten dependencies and such.
PnpWebpackPlugin,
// Prevents users from importing files from outside of src/ (or node_modules/).
// This often causes confusion because we only process files within src/ with babel.
// To fix this, we prevent you from importing files out of src/ -- if you'd like to,
// please link the files into your node_modules/ and let module-resolution kick in.
// Make sure your source files are compiled, as they will not be processed in any way.
new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]),
],
},
resolveLoader: {
plugins: [
// Also related to Plug'n'Play, but this time it tells webpack to load its loaders
// from the current package.
PnpWebpackPlugin.moduleLoader(module),
],
},
module: {
strictExportPresence: true,
rules: [
// Disable require.ensure as it's not a standard language feature.
{ parser: { requireEnsure: false } },
// First, run the linter.
// It's important to do this before Babel processes the JS.
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
enforce: 'pre',
use: [
{
options: {
cache: true,
formatter: require.resolve('react-dev-utils/eslintFormatter'),
eslintPath: require.resolve('eslint'),
resolvePluginsRelativeTo: __dirname,
},
loader: require.resolve('eslint-loader'),
},
],
include: paths.appSrc,
},
{
// "oneOf" will traverse all following loaders until one will
// match the requirements. When no loader matches it will fall
// back to the "file" loader at the end of the loader list.
oneOf: [
// "url" loader works like "file" loader except that it embeds assets
// smaller than specified limit in bytes as data URLs to avoid requests.
// A missing `test` is equivalent to a match.
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
loader: require.resolve('url-loader'),
options: {
limit: imageInlineSizeLimit,
name: 'static/media/[name].[hash:8].[ext]',
},
},
// Process application JS with Babel.
// The preset includes JSX, Flow, TypeScript, and some ESnext features.
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
include: paths.appSrc,
loader: require.resolve('babel-loader'),
options: {
customize: require.resolve(
'babel-preset-react-app/webpack-overrides'
),
plugins: [
[
require.resolve('babel-plugin-named-asset-import'),
{
loaderMap: {
svg: {
ReactComponent:
'@svgr/webpack?-svgo,+titleProp,+ref![path]',
},
},
},
],
],
// This is a feature of `babel-loader` for webpack (not Babel itself).
// It enables caching results in ./node_modules/.cache/babel-loader/
// directory for faster rebuilds.
cacheDirectory: true,
// See #6846 for context on why cacheCompression is disabled
cacheCompression: false,
compact: isEnvProduction,
},
},
// Process any JS outside of the app with Babel.
// Unlike the application JS, we only compile the standard ES features.
{
test: /\.(js|mjs)$/,
exclude: /@babel(?:\/|\\{1,2})runtime/,
loader: require.resolve('babel-loader'),
options: {
babelrc: false,
configFile: false,
compact: false,
presets: [
[
require.resolve('babel-preset-react-app/dependencies'),
{ helpers: true },
],
],
cacheDirectory: true,
// See #6846 for context on why cacheCompression is disabled
cacheCompression: false,
// Babel sourcemaps are needed for debugging into node_modules
// code. Without the options below, debuggers like VSCode
// show incorrect code and set breakpoints on the wrong lines.
sourceMaps: shouldUseSourceMap,
inputSourceMap: shouldUseSourceMap,
},
},
// "postcss" loader applies autoprefixer to our CSS.
// "css" loader resolves paths in CSS and adds assets as dependencies.
// "style" loader turns CSS into JS modules that inject <style> tags.
// In production, we use MiniCSSExtractPlugin to extract that CSS
// to a file, but in development "style" loader enables hot editing
// of CSS.
// By default we support CSS Modules with the extension .module.css
{
test: cssRegex,
exclude: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction && shouldUseSourceMap,
}),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},
// Adds support for CSS Modules (https://github.com/css-modules/css-modules)
// using the extension .module.css
{
test: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: {
getLocalIdent: getCSSModuleLocalIdent,
},
}),
},
// Opt-in support for SASS (using .scss or .sass extensions).
// By default we support SASS Modules with the
// extensions .module.scss or .module.sass
{
test: sassRegex,
exclude: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 3,
sourceMap: isEnvProduction && shouldUseSourceMap,
},
'sass-loader'
),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},
// Adds support for CSS Modules, but using SASS
// using the extension .module.scss or .module.sass
{
test: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 3,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: {
getLocalIdent: getCSSModuleLocalIdent,
},
},
'sass-loader'
),
},
// "file" loader makes sure those assets get served by WebpackDevServer.
// When you `import` an asset, you get its (virtual) filename.
// In production, they would get copied to the `build` folder.
// This loader doesn't use a "test" so it will catch all modules
// that fall through the other loaders.
{
loader: require.resolve('file-loader'),
// Exclude `js` files to keep "css" loader working as it injects
// its runtime that would otherwise be processed through "file" loader.
// Also exclude `html` and `json` extensions so they get processed
// by webpacks internal loaders.
exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
options: {
name: 'static/media/[name].[hash:8].[ext]',
},
},
// ** STOP ** Are you adding a new loader?
// Make sure to add the new loader(s) before the "file" loader.
],
},
],
},
plugins: [
// Generates an `index.html` file with the <script> injected.
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: paths.appHtml,
},
isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined
)
),
// Inlines the webpack runtime script. This script is too small to warrant
// a network request.
// https://github.com/facebook/create-react-app/issues/5358
isEnvProduction &&
shouldInlineRuntimeChunk &&
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime-.+[.]js/]),
// Makes some environment variables available in index.html.
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
// <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
// It will be an empty string unless you specify "homepage"
// in `package.json`, in which case it will be the pathname of that URL.
new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
// This gives some necessary context to module not found errors, such as
// the requesting resource.
new ModuleNotFoundPlugin(paths.appPath),
// Makes some environment variables available to the JS code, for example:
// if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`.
// It is absolutely essential that NODE_ENV is set to production
// during a production build.
// Otherwise React will be compiled in the very slow development mode.
new webpack.DefinePlugin(env.stringified),
// This is necessary to emit hot updates (currently CSS only):
isEnvDevelopment && new webpack.HotModuleReplacementPlugin(),
// Watcher doesn't work well if you mistype casing in a path so we use
// a plugin that prints an error when you attempt to do this.
// See https://github.com/facebook/create-react-app/issues/240
isEnvDevelopment && new CaseSensitivePathsPlugin(),
// If you require a missing module and then `npm install` it, you still have
// to restart the development server for webpack to discover it. This plugin
// makes the discovery automatic so you don't have to restart.
// See https://github.com/facebook/create-react-app/issues/186
isEnvDevelopment &&
new WatchMissingNodeModulesPlugin(paths.appNodeModules),
isEnvProduction &&
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: 'static/css/[name].[contenthash:8].css',
chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
}),
// Generate an asset manifest file with the following content:
// - "files" key: Mapping of all asset filenames to their corresponding
// output file so that tools can pick it up without having to parse
// `index.html`
// - "entrypoints" key: Array of files which are included in `index.html`,
// can be used to reconstruct the HTML if necessary
new ManifestPlugin({
fileName: 'asset-manifest.json',
publicPath: paths.publicUrlOrPath,
generate: (seed, files, entrypoints) => {
const manifestFiles = files.reduce((manifest, file) => {
manifest[file.name] = file.path;
return manifest;
}, seed);
const entrypointFiles = entrypoints.main.filter(
fileName => !fileName.endsWith('.map')
);
return {
files: manifestFiles,
entrypoints: entrypointFiles,
};
},
}),
// Moment.js is an extremely popular library that bundles large locale files
// by default due to how webpack interprets its code. This is a practical
// solution that requires the user to opt into importing specific locales.
// https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
// You can remove this if you don't use Moment.js:
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
// Generate a service worker script that will precache, and keep up to date,
// the HTML & assets that are part of the webpack build.
isEnvProduction &&
new WorkboxWebpackPlugin.GenerateSW({
clientsClaim: true,
exclude: [/\.map$/, /asset-manifest\.json$/],
importWorkboxFrom: 'cdn',
navigateFallback: paths.publicUrlOrPath + 'index.html',
navigateFallbackBlacklist: [
// Exclude URLs starting with /_, as they're likely an API call
new RegExp('^/_'),
// Exclude any URLs whose last part seems to be a file extension
// as they're likely a resource and not a SPA route.
// URLs containing a "?" character won't be blacklisted as they're likely
// a route with query params (e.g. auth callbacks).
new RegExp('/[^/?]+\\.[^/]+$'),
],
}),
// TypeScript type checking
useTypeScript &&
new ForkTsCheckerWebpackPlugin({
typescript: resolve.sync('typescript', {
basedir: paths.appNodeModules,
}),
async: isEnvDevelopment,
useTypescriptIncrementalApi: true,
checkSyntacticErrors: true,
resolveModuleNameModule: process.versions.pnp
? `${__dirname}/pnpTs.js`
: undefined,
resolveTypeReferenceDirectiveModule: process.versions.pnp
? `${__dirname}/pnpTs.js`
: undefined,
tsconfig: paths.appTsConfig,
reportFiles: [
'**',
'!**/__tests__/**',
'!**/?(*.)(spec|test).*',
'!**/src/setupProxy.*',
'!**/src/setupTests.*',
],
silent: true,
// The formatter is invoked directly in WebpackDevServerUtils during development
formatter: isEnvProduction ? typescriptFormatter : undefined,
}),
// Gzip compression.
isEnvProduction &&
new CompressionPlugin({
// asset: "[path].gz[query]",
algorithm: "gzip",
test: /\.js$|\.css$|\.html$/,
threshold: 10240,
minRatio: 0.8
})
].filter(Boolean),
// Some libraries import Node modules but don't use them in the browser.
// Tell webpack to provide empty mocks for them so importing them works.
node: {
module: 'empty',
dgram: 'empty',
dns: 'mock',
fs: 'empty',
http2: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty',
},
// Turn off performance processing because we utilize
// our own hints via the FileSizeReporter
performance: false,
};
};

View File

@@ -1,130 +0,0 @@
'use strict';
const fs = require('fs');
const errorOverlayMiddleware = require('react-dev-utils/errorOverlayMiddleware');
const evalSourceMapMiddleware = require('react-dev-utils/evalSourceMapMiddleware');
const noopServiceWorkerMiddleware = require('react-dev-utils/noopServiceWorkerMiddleware');
const ignoredFiles = require('react-dev-utils/ignoredFiles');
const redirectServedPath = require('react-dev-utils/redirectServedPathMiddleware');
const paths = require('./paths');
const getHttpsConfig = require('./getHttpsConfig');
const host = process.env.HOST || '0.0.0.0';
const sockHost = process.env.WDS_SOCKET_HOST;
const sockPath = process.env.WDS_SOCKET_PATH; // default: '/sockjs-node'
const sockPort = process.env.WDS_SOCKET_PORT;
module.exports = function(proxy, allowedHost) {
return {
// WebpackDevServer 2.4.3 introduced a security fix that prevents remote
// websites from potentially accessing local content through DNS rebinding:
// https://github.com/webpack/webpack-dev-server/issues/887
// https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a
// However, it made several existing use cases such as development in cloud
// environment or subdomains in development significantly more complicated:
// https://github.com/facebook/create-react-app/issues/2271
// https://github.com/facebook/create-react-app/issues/2233
// While we're investigating better solutions, for now we will take a
// compromise. Since our WDS configuration only serves files in the `public`
// folder we won't consider accessing them a vulnerability. However, if you
// use the `proxy` feature, it gets more dangerous because it can expose
// remote code execution vulnerabilities in backends like Django and Rails.
// So we will disable the host check normally, but enable it if you have
// specified the `proxy` setting. Finally, we let you override it if you
// really know what you're doing with a special environment variable.
disableHostCheck:
!proxy || process.env.DANGEROUSLY_DISABLE_HOST_CHECK === 'true',
// Enable gzip compression of generated files.
compress: true,
// Silence WebpackDevServer's own logs since they're generally not useful.
// It will still show compile warnings and errors with this setting.
clientLogLevel: 'none',
// By default WebpackDevServer serves physical files from current directory
// in addition to all the virtual build products that it serves from memory.
// This is confusing because those files wont automatically be available in
// production build folder unless we copy them. However, copying the whole
// project directory is dangerous because we may expose sensitive files.
// Instead, we establish a convention that only files in `public` directory
// get served. Our build script will copy `public` into the `build` folder.
// In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%:
// <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
// In JavaScript code, you can access it with `process.env.PUBLIC_URL`.
// Note that we only recommend to use `public` folder as an escape hatch
// for files like `favicon.ico`, `manifest.json`, and libraries that are
// for some reason broken when imported through webpack. If you just want to
// use an image, put it in `src` and `import` it from JavaScript instead.
contentBase: paths.appPublic,
contentBasePublicPath: paths.publicUrlOrPath,
// By default files from `contentBase` will not trigger a page reload.
watchContentBase: true,
// Enable hot reloading server. It will provide WDS_SOCKET_PATH endpoint
// for the WebpackDevServer client so it can learn when the files were
// updated. The WebpackDevServer client is included as an entry point
// in the webpack development configuration. Note that only changes
// to CSS are currently hot reloaded. JS changes will refresh the browser.
hot: true,
// Use 'ws' instead of 'sockjs-node' on server since we're using native
// websockets in `webpackHotDevClient`.
transportMode: 'ws',
// Prevent a WS client from getting injected as we're already including
// `webpackHotDevClient`.
injectClient: false,
// Enable custom sockjs pathname for websocket connection to hot reloading server.
// Enable custom sockjs hostname, pathname and port for websocket connection
// to hot reloading server.
sockHost,
sockPath,
sockPort,
// It is important to tell WebpackDevServer to use the same "publicPath" path as
// we specified in the webpack config. When homepage is '.', default to serving
// from the root.
// remove last slash so user can land on `/test` instead of `/test/`
publicPath: paths.publicUrlOrPath.slice(0, -1),
// WebpackDevServer is noisy by default so we emit custom message instead
// by listening to the compiler events with `compiler.hooks[...].tap` calls above.
quiet: true,
// Reportedly, this avoids CPU overload on some systems.
// https://github.com/facebook/create-react-app/issues/293
// src/node_modules is not ignored to support absolute imports
// https://github.com/facebook/create-react-app/issues/1065
watchOptions: {
ignored: ignoredFiles(paths.appSrc),
},
https: getHttpsConfig(),
host,
overlay: false,
historyApiFallback: {
// Paths with dots should still use the history fallback.
// See https://github.com/facebook/create-react-app/issues/387.
disableDotRule: true,
index: paths.publicUrlOrPath,
},
public: allowedHost,
// `proxy` is run between `before` and `after` `webpack-dev-server` hooks
proxy,
before(app, server) {
// Keep `evalSourceMapMiddleware` and `errorOverlayMiddleware`
// middlewares before `redirectServedPath` otherwise will not have any effect
// This lets us fetch source contents from webpack for the error overlay
app.use(evalSourceMapMiddleware(server));
// This lets us open files from the runtime error overlay.
app.use(errorOverlayMiddleware());
if (fs.existsSync(paths.proxySetup)) {
// This registers user provided middleware for proxy reasons
require(paths.proxySetup)(app);
}
},
after(app) {
// Redirect to `PUBLIC_URL` or `homepage` from `package.json` if url not match
app.use(redirectServedPath(paths.publicUrlOrPath));
// This service worker file is effectively a 'no-op' that will reset any
// previous service worker registered for the same host:port combination.
// We do this in development to avoid hitting the production cache if
// it used the same host and port.
// https://github.com/facebook/create-react-app/issues/2272#issuecomment-302832432
app.use(noopServiceWorkerMiddleware(paths.publicUrlOrPath));
},
};
};

9
craco.config.js Normal file
View File

@@ -0,0 +1,9 @@
const path = require('path');
module.exports = {
webpack: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
};

13338
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,6 @@
"version": "1.7.1",
"private": true,
"dependencies": {
"@babel/core": "7.8.4",
"@blueprintjs-formik/core": "^0.2.1",
"@blueprintjs-formik/select": "^0.1.4",
"@blueprintjs/core": "^3.50.2",
@@ -14,27 +13,33 @@
"@blueprintjs/timezone": "^3.6.2",
"@casl/ability": "^5.4.3",
"@casl/react": "^2.3.0",
"@craco/craco": "^5.9.0",
"@reduxjs/toolkit": "^1.2.5",
"@sentry/react": "^6.13.2",
"@sentry/tracing": "^6.13.2",
"@svgr/webpack": "4.3.3",
"@tanem/react-nprogress": "^3.0.24",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.4.0",
"@testing-library/user-event": "^7.2.1",
"@types/jest": "^26.0.15",
"@types/js-money": "^0.6.1",
"@types/lodash": "^4.14.172",
"@types/node": "^14.14.9",
"@types/ramda": "^0.28.14",
"@types/react": "^16.14.28",
"@types/react-body-classname": "^1.1.7",
"@types/react-redux": "^7.1.24",
"@types/react-router-dom": "^5.3.3",
"@types/react-transition-group": "^4.4.5",
"@types/styled-components": "^5.1.25",
"@types/yup": "^0.29.13",
"@typescript-eslint/eslint-plugin": "^2.10.0",
"@typescript-eslint/parser": "^2.10.0",
"@welldone-software/why-did-you-render": "^6.0.0-rc.1",
"accounting": "^0.4.1",
"axios": "^0.21.2",
"babel-eslint": "10.0.3",
"babel-jest": "^24.9.0",
"babel-loader": "8.0.6",
"babel-plugin-named-asset-import": "^0.3.6",
"babel-preset-react-app": "^9.1.1",
"basscss": "^8.0.2",
"camelcase": "^5.3.1",
"case-sensitive-paths-webpack-plugin": "2.3.0",
"cross-env": "^7.0.2",
"css-loader": "3.4.2",
"deep-map-keys": "^2.0.1",
@@ -42,20 +47,11 @@
"dependency-graph": "^0.11.0",
"dotenv": "8.2.0",
"dotenv-expand": "5.1.0",
"eslint": "^6.6.0",
"eslint-config-react-app": "^5.2.0",
"eslint-loader": "3.0.3",
"eslint-plugin-flowtype": "4.6.0",
"eslint-plugin-import": "2.20.0",
"eslint-plugin-jsx-a11y": "6.2.3",
"eslint-plugin-react": "7.18.0",
"eslint-plugin-react-hooks": "^1.6.1",
"fast-deep-equal": "^3.1.3",
"file-loader": "4.3.0",
"flow-bin": "^0.123.0",
"formik": "^2.2.5",
"fs-extra": "^8.1.0",
"html-webpack-plugin": "4.0.0-beta.11",
"http-proxy-middleware": "^1.0.0",
"identity-obj-proxy": "3.0.0",
"jest": "24.9.0",
"jest-environment-jsdom-fourteen": "1.0.1",
@@ -63,21 +59,12 @@
"jest-watch-typeahead": "0.4.2",
"js-money": "^0.6.3",
"lodash": "^4.17.15",
"mini-css-extract-plugin": "0.9.0",
"moment": "^2.24.0",
"moment-timezone": "^0.5.33",
"node-sass": "^4.13.1",
"optimize-css-assets-webpack-plugin": "5.0.3",
"pnp-webpack-plugin": "1.6.0",
"postcss-flexbugs-fixes": "4.1.0",
"postcss-loader": "3.0.0",
"postcss-normalize": "8.0.1",
"postcss-preset-env": "6.7.0",
"postcss-rtl": "^1.7.3",
"postcss-safe-parser": "4.0.1",
"path-browserify": "^1.0.1",
"query-string": "^7.1.1",
"ramda": "^0.27.1",
"react": "^16.12.0",
"react": "^16.14.0",
"react-app-polyfill": "^1.0.6",
"react-body-classname": "^1.3.1",
"react-content-loader": "^6.0.1",
@@ -89,9 +76,11 @@
"react-intl-universal": "^2.4.7",
"react-loadable": "^5.5.0",
"react-query": "^3.6.0",
"react-query-devtools": "^2.1.1",
"react-redux": "^7.1.3",
"react-router-breadcrumbs-hoc": "^3.2.10",
"react-router-dom": "^5.2.0",
"react-router-dom": "^5.3.3",
"react-scripts": "^3.4.4",
"react-scroll-sync": "^0.7.1",
"react-scrollbars-custom": "^4.0.21",
"react-sortablejs": "^2.0.11",
@@ -103,31 +92,24 @@
"react-use-context-menu": "^0.1.4",
"react-virtualized": "^9.22.3",
"redux": "^4.0.5",
"redux-devtools": "^3.5.0",
"redux-persist": "^6.0.0",
"redux-thunk": "^2.3.0",
"resolve": "1.15.0",
"resolve-url-loader": "3.1.1",
"rtl-detect": "^1.0.3",
"sass-loader": "8.0.2",
"semver": "6.3.0",
"style-loader": "0.23.1",
"styled-components": "^5.3.1",
"stylis-rtlcss": "^2.1.1",
"terser-webpack-plugin": "2.3.4",
"ts-pnp": "1.1.5",
"url-loader": "2.3.0",
"webpack": "4.41.5",
"webpack-dev-server": "3.10.2",
"webpack-manifest-plugin": "2.2.0",
"workbox-webpack-plugin": "4.3.1",
"typescript": "^4.1.2",
"yup": "^0.28.1"
},
"scripts": {
"start": "PORT=8000 node scripts/start.js",
"start-win": "cross-env PORT=8000 node scripts/start.js",
"build": "node scripts/build.js",
"start": "craco start",
"build": "craco build",
"test": "node scripts/test.js",
"flow": "flow"
"storybook": "start-storybook -p 6006",
"preinstall": "npx npm-force-resolutions"
},
"eslintConfig": {
"extends": "react-app"
@@ -144,23 +126,6 @@
"last 1 safari version"
]
},
"devDependencies": {
"@babel/preset-flow": "^7.9.0",
"@types/jest": "^26.0.15",
"@types/node": "^14.14.9",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/react-router-dom": "^5.1.8",
"@types/styled-components": "^5.1.25",
"@types/yup": "^0.29.13",
"@welldone-software/why-did-you-render": "^6.0.0-rc.1",
"compression-webpack-plugin": "^6.1.0",
"http-proxy-middleware": "^1.0.0",
"postcss-rtlcss": "^1.7.2",
"react-query-devtools": "^2.1.1",
"redux-devtools": "^3.5.0",
"typescript": "^4.1.2"
},
"jest": {
"roots": [
"<rootDir>/src"
@@ -193,7 +158,7 @@
"/Users/ahmed/Documents/Ratteb/client/src"
],
"moduleNameMapper": {
"^react-native$": "react-native-web",
"^react-native$": "react-native-b",
"^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy"
},
"moduleFileExtensions": [
@@ -218,5 +183,8 @@
"react-app"
]
},
"proxy": "http://localhost:3000/"
"proxy": "http://localhost:3000/",
"devDependencies": {
"@types/react-dom": "^16.9.16"
}
}

View File

@@ -1,211 +0,0 @@
'use strict';
// Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = 'production';
process.env.NODE_ENV = 'production';
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', err => {
throw err;
});
// Ensure environment variables are read.
require('../config/env');
const path = require('path');
const chalk = require('react-dev-utils/chalk');
const fs = require('fs-extra');
const webpack = require('webpack');
const configFactory = require('../config/webpack.config');
const paths = require('../config/paths');
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
const printHostingInstructions = require('react-dev-utils/printHostingInstructions');
const FileSizeReporter = require('react-dev-utils/FileSizeReporter');
const printBuildError = require('react-dev-utils/printBuildError');
const measureFileSizesBeforeBuild =
FileSizeReporter.measureFileSizesBeforeBuild;
const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild;
const useYarn = fs.existsSync(paths.yarnLockFile);
// These sizes are pretty large. We'll warn for bundles exceeding them.
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;
const isInteractive = process.stdout.isTTY;
// Warn and crash if required files are missing
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
process.exit(1);
}
// Generate configuration
const config = configFactory('production');
// We require that you explicitly set browsers and do not fall back to
// browserslist defaults.
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
checkBrowsers(paths.appPath, isInteractive)
.then(() => {
// First, read the current file sizes in build directory.
// This lets us display how much they changed later.
return measureFileSizesBeforeBuild(paths.appBuild);
})
.then(previousFileSizes => {
// Remove all content but keep the directory so that
// if you're in it, you don't end up in Trash
fs.emptyDirSync(paths.appBuild);
// Merge with the public folder
copyPublicFolder();
// Start the webpack build
return build(previousFileSizes);
})
.then(
({ stats, previousFileSizes, warnings }) => {
if (warnings.length) {
console.log(chalk.yellow('Compiled with warnings.\n'));
console.log(warnings.join('\n\n'));
console.log(
'\nSearch for the ' +
chalk.underline(chalk.yellow('keywords')) +
' to learn more about each warning.'
);
console.log(
'To ignore, add ' +
chalk.cyan('// eslint-disable-next-line') +
' to the line before.\n'
);
} else {
console.log(chalk.green('Compiled successfully.\n'));
}
console.log('File sizes after gzip:\n');
printFileSizesAfterBuild(
stats,
previousFileSizes,
paths.appBuild,
WARN_AFTER_BUNDLE_GZIP_SIZE,
WARN_AFTER_CHUNK_GZIP_SIZE
);
console.log();
const appPackage = require(paths.appPackageJson);
const publicUrl = paths.publicUrlOrPath;
const publicPath = config.output.publicPath;
const buildFolder = path.relative(process.cwd(), paths.appBuild);
printHostingInstructions(
appPackage,
publicUrl,
publicPath,
buildFolder,
useYarn
);
},
err => {
const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true';
if (tscCompileOnError) {
console.log(
chalk.yellow(
'Compiled with the following type errors (you may want to check these before deploying your app):\n'
)
);
printBuildError(err);
} else {
console.log(chalk.red('Failed to compile.\n'));
printBuildError(err);
process.exit(1);
}
}
)
.catch(err => {
if (err && err.message) {
console.log(err.message);
}
process.exit(1);
});
// Create the production build and print the deployment instructions.
function build(previousFileSizes) {
// We used to support resolving modules according to `NODE_PATH`.
// This now has been deprecated in favor of jsconfig/tsconfig.json
// This lets you use absolute paths in imports inside large monorepos:
if (process.env.NODE_PATH) {
console.log(
chalk.yellow(
'Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.'
)
);
console.log();
}
console.log('Creating an optimized production build...');
const compiler = webpack(config);
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
let messages;
if (err) {
if (!err.message) {
return reject(err);
}
let errMessage = err.message;
// Add additional information for postcss errors
if (Object.prototype.hasOwnProperty.call(err, 'postcssNode')) {
errMessage +=
'\nCompileError: Begins at CSS selector ' +
err['postcssNode'].selector;
}
messages = formatWebpackMessages({
errors: [errMessage],
warnings: [],
});
} else {
messages = formatWebpackMessages(
stats.toJson({ all: false, warnings: true, errors: true })
);
}
if (messages.errors.length) {
// Only keep the first error. Others are often indicative
// of the same problem, but confuse the reader with noise.
if (messages.errors.length > 1) {
messages.errors.length = 1;
}
return reject(new Error(messages.errors.join('\n\n')));
}
if (
process.env.CI &&
(typeof process.env.CI !== 'string' ||
process.env.CI.toLowerCase() !== 'false') &&
messages.warnings.length
) {
console.log(
chalk.yellow(
'\nTreating warnings as errors because process.env.CI = true.\n' +
'Most CI servers set it automatically.\n'
)
);
return reject(new Error(messages.warnings.join('\n\n')));
}
return resolve({
stats,
previousFileSizes,
warnings: messages.warnings,
});
});
});
}
function copyPublicFolder() {
fs.copySync(paths.appPublic, paths.appBuild, {
dereference: true,
filter: file => file !== paths.appHtml,
});
}

View File

@@ -1,157 +0,0 @@
'use strict';
// Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = 'development';
process.env.NODE_ENV = 'development';
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', err => {
throw err;
});
// Ensure environment variables are read.
require('../config/env');
const fs = require('fs');
const chalk = require('react-dev-utils/chalk');
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const clearConsole = require('react-dev-utils/clearConsole');
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
const {
choosePort,
createCompiler,
prepareProxy,
prepareUrls,
} = require('react-dev-utils/WebpackDevServerUtils');
const openBrowser = require('react-dev-utils/openBrowser');
const paths = require('../config/paths');
const configFactory = require('../config/webpack.config');
const createDevServerConfig = require('../config/webpackDevServer.config');
const useYarn = fs.existsSync(paths.yarnLockFile);
const isInteractive = process.stdout.isTTY;
// Warn and crash if required files are missing
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
process.exit(1);
}
// Tools like Cloud9 rely on this.
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
const HOST = process.env.HOST || '0.0.0.0';
if (process.env.HOST) {
console.log(
chalk.cyan(
`Attempting to bind to HOST environment variable: ${chalk.yellow(
chalk.bold(process.env.HOST)
)}`
)
);
console.log(
`If this was unintentional, check that you haven't mistakenly set it in your shell.`
);
console.log(
`Learn more here: ${chalk.yellow('https://bit.ly/CRA-advanced-config')}`
);
console.log();
}
// We require that you explicitly set browsers and do not fall back to
// browserslist defaults.
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
checkBrowsers(paths.appPath, isInteractive)
.then(() => {
// We attempt to use the default port but if it is busy, we offer the user to
// run on a different port. `choosePort()` Promise resolves to the next free port.
return choosePort(HOST, DEFAULT_PORT);
})
.then(port => {
if (port == null) {
// We have not found a port.
return;
}
const config = configFactory('development');
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
const appName = require(paths.appPackageJson).name;
const useTypeScript = fs.existsSync(paths.appTsConfig);
const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true';
const urls = prepareUrls(
protocol,
HOST,
port,
paths.publicUrlOrPath.slice(0, -1)
);
const devSocket = {
warnings: warnings =>
devServer.sockWrite(devServer.sockets, 'warnings', warnings),
errors: errors =>
devServer.sockWrite(devServer.sockets, 'errors', errors),
};
// Create a webpack compiler that is configured with custom messages.
const compiler = createCompiler({
appName,
config,
devSocket,
urls,
useYarn,
useTypeScript,
tscCompileOnError,
webpack,
});
// Load proxy config
const proxySetting = require(paths.appPackageJson).proxy;
const proxyConfig = prepareProxy(
proxySetting,
paths.appPublic,
paths.publicUrlOrPath
);
// Serve webpack assets generated by the compiler over a web server.
const serverConfig = createDevServerConfig(
proxyConfig,
urls.lanUrlForConfig
);
const devServer = new WebpackDevServer(compiler, serverConfig);
// Launch WebpackDevServer.
devServer.listen(port, HOST, err => {
if (err) {
return console.log(err);
}
if (isInteractive) {
clearConsole();
}
// We used to support resolving modules according to `NODE_PATH`.
// This now has been deprecated in favor of jsconfig/tsconfig.json
// This lets you use absolute paths in imports inside large monorepos:
if (process.env.NODE_PATH) {
console.log(
chalk.yellow(
'Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.'
)
);
console.log();
}
console.log(chalk.cyan('Starting the development server...\n'));
openBrowser(urls.localUrlForBrowser);
});
['SIGINT', 'SIGTERM'].forEach(function(sig) {
process.on(sig, function() {
devServer.close();
process.exit();
});
});
})
.catch(err => {
if (err && err.message) {
console.log(err.message);
}
process.exit(1);
});

View File

@@ -1,53 +0,0 @@
'use strict';
// Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = 'test';
process.env.NODE_ENV = 'test';
process.env.PUBLIC_URL = '';
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', err => {
throw err;
});
// Ensure environment variables are read.
require('../config/env');
const jest = require('jest');
const execSync = require('child_process').execSync;
let argv = process.argv.slice(2);
function isInGitRepository() {
try {
execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
return true;
} catch (e) {
return false;
}
}
function isInMercurialRepository() {
try {
execSync('hg --cwd . root', { stdio: 'ignore' });
return true;
} catch (e) {
return false;
}
}
// Watch unless on CI or explicitly running all tests
if (
!process.env.CI &&
argv.indexOf('--watchAll') === -1 &&
argv.indexOf('--watchAll=false') === -1
) {
// https://github.com/facebook/create-react-app/issues/5210
const hasSourceControl = isInGitRepository() || isInMercurialRepository();
argv.push(hasSourceControl ? '--watch' : '--watchAll');
}
jest.run(argv);

View File

@@ -1,22 +0,0 @@
export const DRAWERS = {
ESTIMATE_DRAWER: 'estimate-drawer',
MANUAL_JOURNAL_DRAWER: 'journal-drawer',
INVOICE_DRAWER: 'invoice-drawer',
RECEIPT_DRAWER: 'receipt-drawer',
PAYMENT_RECEIVE_DRAWER: 'payment-receive-drawer',
ACCOUNT_DRAWER: 'account-drawer',
JOURNAL_DRAWER: 'journal-drawer',
EXPENSE_DRAWER: 'expense-drawer',
BILL_DRAWER: 'bill-drawer',
INVENTORY_ADJUSTMENT_DRAWER: 'inventory-adjustment-drawer',
CASHFLOW_TRNASACTION_DRAWER: 'cashflow-transaction-drawer',
QUICK_WRITE_VENDOR: 'quick-write-vendor',
QUICK_CREATE_CUSTOMER: 'quick-create-customer',
QUICK_CREATE_ITEM: 'quick-create-item',
CREDIT_NOTE_DETAIL_DRAWER: 'credit-note-detail-drawer',
VENDOR_CREDIT_DETAIL_DRAWER: 'vendor-credit-detail-drawer',
REFUND_CREDIT_NOTE_DETAIL_DRAWER:'refund-credit-detail-drawer',
REFUND_VENDOR_CREDIT_DETAIL_DRAWER:'refund-vendor-detail-drawer',
WAREHOUSE_TRANSFER_DETAIL_DRAWER:'warehouse-transfer-detail-drawer'
};

View File

@@ -1,175 +0,0 @@
import React from 'react';
import { FormattedMessage as T } from 'components';
import {
SaleInvoiceAction,
SaleEstimateAction,
AbilitySubject,
SaleReceiptAction,
CustomerAction,
PaymentReceiveAction,
BillAction,
VendorAction,
PaymentMadeAction,
AccountAction,
ManualJournalAction,
ExpenseAction,
ItemAction,
ReportsAction,
} from '../common/abilityOption';
export const accountsReceivable = [
{
sectionTitle: <T id={'accounts_receivable_a_r'} />,
shortcuts: [
{
title: <T id={'sales_invoices'} />,
description: <T id={'tracking_sales_invoices_with_your_customers'} />,
link: '/invoices',
subject: AbilitySubject.Invoice,
ability: SaleInvoiceAction.View,
},
{
title: <T id={'sales_estimates'} />,
description: <T id={'manage_your_sales_estimates_to_create_quotes'} />,
link: '/estimates',
subject: AbilitySubject.Estimate,
ability: SaleEstimateAction.View,
},
{
title: <T id={'sales_receipts'} />,
description: <T id={'manage_sales_receipts_for_sales_that_get_paid'} />,
link: '/receipts',
subject: AbilitySubject.Receipt,
ability: SaleReceiptAction.View,
},
{
title: <T id={'customers'} />,
description: <T id={'manage_the_customers_relations_with_customer'} />,
link: '/customers',
subject: AbilitySubject.Customer,
ability: CustomerAction.View,
},
{
title: <T id={'customers_payments'} />,
description: (
<T id={'manage_payment_transactions_from_your_customers'} />
),
link: '/payment-receives',
subject: AbilitySubject.PaymentReceive,
ability: PaymentReceiveAction.View,
},
],
},
];
export const accountsPayable = [
{
sectionTitle: <T id={'accounts_payable_a_p'} />,
shortcuts: [
{
title: <T id={'purchase_invoices'} />,
description: (
<T id={'manage_the_purchase_invoices_with_your_vendors'} />
),
link: '/bills',
subject: AbilitySubject.Bill,
ability: BillAction.View,
},
{
title: <T id={'vendors'} />,
description: (
<T id={'manage_the_vendors_relations_with_vendor_relations'} />
),
link: '/vendors',
subject: AbilitySubject.Vendor,
ability: VendorAction.View,
},
{
title: <T id={'vendors_payments'} />,
description: <T id={'manage_payments_transactions_to_your_vendors'} />,
link: '/payment-mades',
subject: AbilitySubject.PaymentMade,
ability: PaymentMadeAction.View,
},
],
},
];
export const financialAccounting = [
{
sectionTitle: <T id={'financial_accounting'} />,
shortcuts: [
{
title: <T id={'chart_of_accounts'} />,
description: (
<T
id={
'manage_your_accounts_chart_to_record_your_transactions_and_categories'
}
/>
),
link: '/accounts',
subject: AbilitySubject.Account,
ability: AccountAction.View,
},
{
title: <T id={'manual_journal'} />,
description: (
<T id={'manage_manual_journal_transactions_on_accounts'} />
),
link: '/manual-journals',
subject: AbilitySubject.ManualJournal,
ability: ManualJournalAction.View,
},
{
title: <T id={'expenses'} />,
description: (
<T id={'track_your_indirect_expenses_under_specific_categories'} />
),
link: '/expenses',
subject: AbilitySubject.Expense,
ability: ExpenseAction.View,
},
{
title: <T id={'financial_statements'} />,
description: (
<T id={'show_financial_reports_about_your_organization'} />
),
link: '/financial-reports',
subject: AbilitySubject.Report,
ability: ReportsAction.ALL,
},
],
},
];
export const productsServices = [
{
sectionTitle: <T id={'products_services_inventory'} />,
shortcuts: [
{
title: <T id={'products_services'} />,
description: (
<T id={'manage_your_products_inventory_or_non_inventory'} />
),
link: '/items',
subject: AbilitySubject.Item,
ability: ItemAction.View,
},
{
title: <T id={'products_services_categories'} />,
description: <T id={'group_your_products_and_service'} />,
link: 'items/categories',
},
{
title: <T id={'inventory_adjustments'} />,
description: (
<T id={'manage_your_inventory_adjustment_of_inventory_items'} />
),
link: '/inventory-adjustments',
subject: AbilitySubject.InventoryAdjustment,
ability: SaleInvoiceAction.View,
},
],
},
];

View File

@@ -1,6 +0,0 @@
export * from './accountTypes';
export * from './TableStyle';
export * from './features';
export * from './cellTypes';
export const Align = { Left: 'left', Right: 'right', Center: 'center' };

View File

@@ -1,51 +0,0 @@
import intl from 'react-intl-universal';
import {
AbilitySubject,
SaleEstimateAction,
SaleReceiptAction,
SaleInvoiceAction,
BillAction,
} from '../common/abilityOption';
import { useAbilitiesFilter } from '../hooks';
export const getItemPaymentTransactions = () => [
{
name: 'invoices',
label: intl.get('invoices'),
permission: {
subject: AbilitySubject.Invoice,
ability: SaleInvoiceAction.View,
},
},
{
name: 'estimates',
label: intl.get('estimates'),
permission: {
subject: AbilitySubject.Estimate,
ability: SaleEstimateAction.View,
},
},
{
name: 'receipts',
label: intl.get('receipts'),
permission: {
subject: AbilitySubject.Receipt,
ability: SaleReceiptAction.View,
},
},
{
name: 'bills',
label: intl.get('bills'),
permission: {
subject: AbilitySubject.Bill,
ability: BillAction.View,
},
},
];
export const useGetItemPaymentTransactionsMenu = () => {
const itemTransactionMenu = getItemPaymentTransactions();
const abilitiesFilter = useAbilitiesFilter();
return abilitiesFilter(itemTransactionMenu);
};

View File

@@ -1,71 +0,0 @@
import intl from 'react-intl-universal';
import {
AbilitySubject,
SaleInvoiceAction,
CustomerAction,
VendorAction,
ManualJournalAction,
ExpenseAction,
} from '../common/abilityOption';
import { useAbilitiesFilter } from '../hooks';
export const getQuickNewActions = () => [
{
path: 'invoices/new',
name: intl.get('sale_invoice'),
permission: {
subject: AbilitySubject.Invoice,
ability: SaleInvoiceAction.Create,
},
},
{
path: 'bills/new',
name: intl.get('purchase_invoice'),
permission: {
subject: AbilitySubject.Invoice,
ability: SaleInvoiceAction.Create,
},
},
{
path: 'make-journal-entry',
name: intl.get('manual_journal'),
permission: {
subject: AbilitySubject.ManualJournal,
ability: ManualJournalAction.Create,
},
},
{
path: 'expenses/new',
name: intl.get('expense'),
permission: {
subject: AbilitySubject.Expense,
ability: ExpenseAction.Create,
},
},
{
path: 'customers/new',
name: intl.get('customer'),
permission: {
subject: AbilitySubject.Customer,
ability: CustomerAction.Create,
},
},
{
path: 'vendors/new',
name: intl.get('vendor'),
permission: {
subject: AbilitySubject.Vendor,
ability: VendorAction.Vendor,
},
},
];
/**
* Retrieve the dashboard quick new menu items.
*/
export const useGetQuickNewMenu = () => {
const quickNewMenu = getQuickNewActions();
const abilitiesFilter = useAbilitiesFilter();
return abilitiesFilter(quickNewMenu);
};

View File

@@ -0,0 +1,30 @@
import React from 'react';
import { MenuItem } from '@blueprintjs/core';
import { MultiSelect } from '../MultiSelectTaggable';
export function AccountsMultiSelect({ ...multiSelectProps }) {
return (
<MultiSelect
itemRenderer={(
item,
{ active, selected, handleClick, modifiers, query },
) => {
return (
<MenuItem
active={active}
icon={selected ? 'tick' : 'blank'}
text={item.name}
label={item.code}
key={item.id}
onClick={handleClick}
/>
);
}}
popoverProps={{ minimal: true }}
fill={true}
tagRenderer={(item) => item.name}
resetOnSelect={true}
{...multiSelectProps}
/>
);
}

View File

@@ -0,0 +1,174 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import * as R from 'ramda';
import { MenuItemNestedText, FormattedMessage as T } from '@/components';
import { nestedArrayToflatten, filterAccountsByQuery } from '@/utils';
import { CLASSES } from '@/constants/classes';
import withDialogActions from '@/containers/Dialog/withDialogActions';
// Create new account renderer.
const createNewItemRenderer = (query, active, handleClick) => {
return (
<MenuItem
icon="add"
text={intl.get('list.create', { value: `"${query}"` })}
active={active}
onClick={handleClick}
/>
);
};
// Create new item from the given query string.
const createNewItemFromQuery = (name) => {
return {
name,
};
};
// Filters accounts items.
const filterAccountsPredicater = (query, account, _index, exactMatch) => {
const normalizedTitle = account.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
}
};
/**
* Accounts select list.
*/
function AccountsSelectListRoot({
// #withDialogActions
openDialog,
// #ownProps
accounts,
initialAccountId,
selectedAccountId,
defaultSelectText = 'Select account',
onAccountSelected,
disabled = false,
popoverFill = false,
filterByParentTypes,
filterByTypes,
filterByNormal,
filterByRootTypes,
allowCreate,
buttonProps = {},
}) {
const flattenAccounts = useMemo(
() => nestedArrayToflatten(accounts),
[accounts],
);
// Filters accounts based on filter props.
const filteredAccounts = useMemo(() => {
let filteredAccounts = filterAccountsByQuery(flattenAccounts, {
filterByRootTypes,
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],
);
// Select account item.
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}
/>
);
}, []);
// Handle the account item select.
const handleAccountSelect = useCallback(
(account) => {
if (account.id) {
setSelectedAccount({ ...account });
onAccountSelected && onAccountSelected(account);
} else {
openDialog('account-form');
}
},
[setSelectedAccount, onAccountSelected, openDialog],
);
// Maybe inject new item props to select component.
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
const maybeCreateNewItemFromQuery = allowCreate
? createNewItemFromQuery
: null;
return (
<Select
items={filteredAccounts}
noResults={<MenuItem disabled={true} text={<T id={'no_accounts'} />} />}
itemRenderer={accountItem}
itemPredicate={filterAccountsPredicater}
popoverProps={{
minimal: true,
usePortal: !popoverFill,
inline: popoverFill,
}}
filterable={true}
onItemSelect={handleAccountSelect}
disabled={disabled}
className={classNames('form-group--select-list', {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
createNewItemRenderer={maybeCreateNewItemRenderer}
createNewItemFromQuery={maybeCreateNewItemFromQuery}
>
<Button
disabled={disabled}
text={selectedAccount ? selectedAccount.name : defaultSelectText}
{...buttonProps}
/>
</Select>
);
}
export const AccountsSelectList = R.compose(withDialogActions)(
AccountsSelectListRoot,
);

View File

@@ -0,0 +1,173 @@
import React, { useState, useCallback, useEffect, 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 { MenuItemNestedText, FormattedMessage as T } from '@/components';
import { nestedArrayToflatten, filterAccountsByQuery } from '@/utils';
import withDialogActions from '@/containers/Dialog/withDialogActions';
// Create new account renderer.
const createNewItemRenderer = (query, active, handleClick) => {
return (
<MenuItem
icon="add"
text={intl.get('list.create', { value: `"${query}"` })}
active={active}
onClick={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 '';
};
// Filters accounts items.
const filterAccountsPredicater = (query, account, _index, exactMatch) => {
const normalizedTitle = account.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
}
};
/**
* Accounts suggest field.
*/
function AccountsSuggestFieldRoot({
// #withDialogActions
openDialog,
// #ownProps
accounts,
initialAccountId,
selectedAccountId,
defaultSelectText = intl.formatMessage({ id: 'select_account' }),
popoverFill = false,
onAccountSelected,
filterByParentTypes = [],
filterByTypes = [],
filterByNormal,
filterByRootTypes = [],
allowCreate,
...suggestProps
}) {
const flattenAccounts = useMemo(
() => nestedArrayToflatten(accounts),
[accounts],
);
// Filters accounts based on filter props.
const filteredAccounts = useMemo(() => {
let filteredAccounts = filterAccountsByQuery(flattenAccounts, {
filterByRootTypes,
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],
);
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 {
openDialog('account-form');
}
},
[setSelectedAccount, onAccountSelected, openDialog],
);
// Maybe inject new item props to select component.
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
const maybeCreateNewItemFromQuery = allowCreate
? createNewItemFromQuery
: null;
return (
<Suggest
items={filteredAccounts}
noResults={<MenuItem disabled={true} text={<T id={'no_accounts'} />} />}
itemRenderer={accountItem}
itemPredicate={filterAccountsPredicater}
onItemSelect={handleAccountSelect}
selectedItem={selectedAccount}
inputProps={{ placeholder: defaultSelectText }}
resetOnClose={true}
fill={true}
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}
/>
);
}
export const AccountsSuggestField = R.compose(withDialogActions)(
AccountsSuggestFieldRoot,
);

View File

@@ -0,0 +1,48 @@
import React, { useCallback } from 'react';
import classNames from 'classnames';
import { ListSelect } from '@/components';
import { CLASSES } from '@/constants/classes';
export function AccountsTypesSelect({
accountsTypes,
selectedTypeId,
defaultSelectText = 'Select account type',
onTypeSelected,
disabled = false,
popoverFill = false,
...restProps
}) {
// Filters accounts types items.
const filterAccountTypeItems = (query, accountType, _index, exactMatch) => {
const normalizedTitle = accountType.label.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return normalizedTitle.indexOf(normalizedQuery) >= 0;
}
};
// Handle item selected.
const handleItemSelected = (accountType) => {
onTypeSelected && onTypeSelected(accountType);
};
return (
<ListSelect
items={accountsTypes}
selectedItemProp={'key'}
selectedItem={selectedTypeId}
textProp={'label'}
defaultText={defaultSelectText}
onItemSelect={handleItemSelected}
itemPredicate={filterAccountTypeItems}
disabled={disabled}
className={classNames('form-group--select-list', {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
{...restProps}
/>
);
}

View File

@@ -1 +0,0 @@
export * from './AccountMultiSelect';

View File

@@ -0,0 +1,5 @@
export * from './AccountMultiSelect';
export * from './AccountsMultiSelect';
export * from './AccountsSelectList';
export * from './AccountsSuggestField';
export * from './AccountsTypesSelect';

View File

@@ -1,30 +0,0 @@
import React from 'react';
import { MenuItem } from '@blueprintjs/core';
import { MultiSelect } from './MultiSelectTaggable';
export default function AccountsMultiSelect({ ...multiSelectProps }) {
return (
<MultiSelect
itemRenderer={(
item,
{ active, selected, handleClick, modifiers, query },
) => {
return (
<MenuItem
active={active}
icon={selected ? 'tick' : 'blank'}
text={item.name}
label={item.code}
key={item.id}
onClick={handleClick}
/>
);
}}
popoverProps={{ minimal: true }}
fill={true}
tagRenderer={(item) => item.name}
resetOnSelect={true}
{...multiSelectProps}
/>
);
}

View File

@@ -1,173 +0,0 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import * as R from 'ramda';
import classNames from 'classnames';
import intl from 'react-intl-universal'
import { MenuItemNestedText, FormattedMessage as T } from 'components';
import { filterAccountsByQuery } from './utils';
import { nestedArrayToflatten } from 'utils';
import { CLASSES } from 'common/classes';
import withDialogActions from 'containers/Dialog/withDialogActions';
// Create new account renderer.
const createNewItemRenderer = (query, active, handleClick) => {
return (
<MenuItem
icon="add"
text={intl.get('list.create', { value: `"${query}"` })}
active={active}
onClick={handleClick}
/>
);
};
// Create new item from the given query string.
const createNewItemFromQuery = (name) => {
return {
name,
};
};
// Filters accounts items.
const filterAccountsPredicater = (query, account, _index, exactMatch) => {
const normalizedTitle = account.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
}
};
/**
* Accounts select list.
*/
function AccountsSelectList({
// #withDialogActions
openDialog,
// #ownProps
accounts,
initialAccountId,
selectedAccountId,
defaultSelectText = 'Select account',
onAccountSelected,
disabled = false,
popoverFill = false,
filterByParentTypes,
filterByTypes,
filterByNormal,
filterByRootTypes,
allowCreate,
buttonProps = {},
}) {
const flattenAccounts = useMemo(
() => nestedArrayToflatten(accounts),
[accounts],
);
// Filters accounts based on filter props.
const filteredAccounts = useMemo(() => {
let filteredAccounts = filterAccountsByQuery(flattenAccounts, {
filterByRootTypes,
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],
);
// Select account item.
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}
/>
);
}, []);
// Handle the account item select.
const handleAccountSelect = useCallback(
(account) => {
if (account.id) {
setSelectedAccount({ ...account });
onAccountSelected && onAccountSelected(account);
} else {
openDialog('account-form');
}
},
[setSelectedAccount, onAccountSelected, openDialog],
);
// Maybe inject new item props to select component.
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
const maybeCreateNewItemFromQuery = allowCreate
? createNewItemFromQuery
: null;
return (
<Select
items={filteredAccounts}
noResults={<MenuItem disabled={true} text={<T id={'no_accounts'} />} />}
itemRenderer={accountItem}
itemPredicate={filterAccountsPredicater}
popoverProps={{
minimal: true,
usePortal: !popoverFill,
inline: popoverFill,
}}
filterable={true}
onItemSelect={handleAccountSelect}
disabled={disabled}
className={classNames('form-group--select-list', {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
createNewItemRenderer={maybeCreateNewItemRenderer}
createNewItemFromQuery={maybeCreateNewItemFromQuery}
>
<Button
disabled={disabled}
text={selectedAccount ? selectedAccount.name : defaultSelectText}
{...buttonProps}
/>
</Select>
);
}
export default R.compose(withDialogActions)(AccountsSelectList);

View File

@@ -1,173 +0,0 @@
import React, { useState, useCallback, useEffect, useMemo } from 'react';
import { MenuItem } from '@blueprintjs/core';
import { Suggest } from '@blueprintjs/select';
import intl from 'react-intl-universal';
import * as R from 'ramda';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { MenuItemNestedText, FormattedMessage as T } from 'components';
import { filterAccountsByQuery } from './utils';
import { nestedArrayToflatten } from 'utils';
import withDialogActions from 'containers/Dialog/withDialogActions';
// Create new account renderer.
const createNewItemRenderer = (query, active, handleClick) => {
return (
<MenuItem
icon="add"
text={intl.get('list.create', { value: `"${query}"` })}
active={active}
onClick={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 '';
};
// Filters accounts items.
const filterAccountsPredicater = (query, account, _index, exactMatch) => {
const normalizedTitle = account.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
}
};
/**
* Accounts suggest field.
*/
function AccountsSuggestField({
// #withDialogActions
openDialog,
// #ownProps
accounts,
initialAccountId,
selectedAccountId,
defaultSelectText = intl.formatMessage({ id: 'select_account' }),
popoverFill = false,
onAccountSelected,
filterByParentTypes = [],
filterByTypes = [],
filterByNormal,
filterByRootTypes = [],
allowCreate,
...suggestProps
}) {
const flattenAccounts = useMemo(
() => nestedArrayToflatten(accounts),
[accounts],
);
// Filters accounts based on filter props.
const filteredAccounts = useMemo(() => {
let filteredAccounts = filterAccountsByQuery(flattenAccounts, {
filterByRootTypes,
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],
);
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 {
openDialog('account-form');
}
},
[setSelectedAccount, onAccountSelected, openDialog],
);
// Maybe inject new item props to select component.
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
const maybeCreateNewItemFromQuery = allowCreate
? createNewItemFromQuery
: null;
return (
<Suggest
items={filteredAccounts}
noResults={<MenuItem disabled={true} text={<T id={'no_accounts'} />} />}
itemRenderer={accountItem}
itemPredicate={filterAccountsPredicater}
onItemSelect={handleAccountSelect}
selectedItem={selectedAccount}
inputProps={{ placeholder: defaultSelectText }}
resetOnClose={true}
fill={true}
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}
/>
);
}
export default R.compose(withDialogActions)(AccountsSuggestField);

View File

@@ -1,48 +0,0 @@
import React, { useCallback } from 'react';
import classNames from 'classnames';
import { ListSelect } from 'components';
import { CLASSES } from 'common/classes';
export default function AccountsTypesSelect({
accountsTypes,
selectedTypeId,
defaultSelectText = 'Select account type',
onTypeSelected,
disabled = false,
popoverFill = false,
...restProps
}) {
// Filters accounts types items.
const filterAccountTypeItems = (query, accountType, _index, exactMatch) => {
const normalizedTitle = accountType.label.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return normalizedTitle.indexOf(normalizedQuery) >= 0;
}
};
// Handle item selected.
const handleItemSelected = (accountType) => {
onTypeSelected && onTypeSelected(accountType);
};
return (
<ListSelect
items={accountsTypes}
selectedItemProp={'key'}
selectedItem={selectedTypeId}
textProp={'label'}
defaultText={defaultSelectText}
onItemSelect={handleItemSelected}
itemPredicate={filterAccountTypeItems}
disabled={disabled}
className={classNames('form-group--select-list', {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
{...restProps}
/>
);
}

View File

@@ -1,27 +0,0 @@
import React from 'react';
import { Classes } from '@blueprintjs/core';
import ListSelect from '../ListSelect';
import { getConditionTypeCompatators } from './utils';
export default function DynamicFilterCompatatorField({
dataType,
...restProps
}) {
const options = getConditionTypeCompatators(dataType);
return (
<ListSelect
textProp={'label'}
selectedItemProp={'value'}
items={options}
className={Classes.FILL}
filterable={false}
popoverProps={{
inline: true,
minimal: true,
captureDismiss: true,
}}
{...restProps}
/>
);
}

View File

@@ -0,0 +1,27 @@
import React from 'react';
import { Classes } from '@blueprintjs/core';
import { ListSelect } from '../Select';
import { getConditionTypeCompatators } from './utils';
export default function DynamicFilterCompatatorField({
dataType,
...restProps
}) {
const options = getConditionTypeCompatators(dataType);
return (
<ListSelect
textProp={'label'}
selectedItemProp={'value'}
items={options}
className={Classes.FILL}
filterable={false}
popoverProps={{
inline: true,
minimal: true,
captureDismiss: true,
}}
{...restProps}
/>
);
}

View File

@@ -1,397 +0,0 @@
import React from 'react';
import { Formik, FastField, FieldArray, useFormikContext } from 'formik';
import {
Button,
FormGroup,
Classes,
InputGroup,
MenuItem,
} from '@blueprintjs/core';
import { get, first, defaultTo, isEqual, isEmpty } from 'lodash';
import intl from 'react-intl-universal';
import { Choose, Icon, FormattedMessage as T, ListSelect } from 'components';
import { useUpdateEffect } from 'hooks';
import {
AdvancedFilterDropdownProvider,
FilterConditionProvider,
useFilterCondition,
useAdvancedFilterContext,
} from './AdvancedFilterDropdownContext';
import AdvancedFilterCompatatorField from './AdvancedFilterCompatatorField';
import AdvancedFilterValueField from './AdvancedFilterValueField';
import {
filterConditionRoles,
getConditionalsOptions,
transformFieldsToOptions,
shouldFilterValueFieldUpdate,
getConditionTypeCompatators,
} from './utils';
import { getFilterDropdownSchema } from './AdvancedFilter.schema';
import { useAdvancedFilterAutoSubmit } from './components';
/**
* Condition item list renderer.
*/
function ConditionItemRenderer(condition, { handleClick, modifiers, query }) {
return (
<MenuItem
text={
<>
<div>{condition.label}</div>
<span className="text-hint">{condition.text}</span>
</>
}
key={condition.value}
onClick={handleClick}
/>
);
}
/**
* Filter condition field.
*/
function FilterConditionField() {
const conditionalsOptions = getConditionalsOptions();
const { conditionIndex, getConditionFieldPath } = useFilterCondition();
const conditionFieldPath = getConditionFieldPath('condition');
return (
<FastField name={conditionFieldPath}>
{({ form, field }) => (
<FormGroup className={'form-group--condition'}>
<Choose>
<Choose.When condition={conditionIndex === 0}>
<InputGroup disabled value={intl.get('filter.when')} />
</Choose.When>
<Choose.Otherwise>
<ListSelect
selectedItem={field.value}
textProp={'label'}
selectedItemProp={'value'}
labelProp={'text'}
items={conditionalsOptions}
className={Classes.FILL}
filterable={false}
onItemSelect={(option) => {
form.setFieldValue(conditionFieldPath, option.value);
}}
popoverProps={{
inline: true,
minimal: true,
captureDismiss: true,
}}
itemRenderer={ConditionItemRenderer}
/>
</Choose.Otherwise>
</Choose>
</FormGroup>
)}
</FastField>
);
}
/**
* Compatator field.
*/
function FilterCompatatorFilter() {
const { getConditionFieldPath, fieldMeta } = useFilterCondition();
const comparatorFieldPath = getConditionFieldPath('comparator');
const fieldType = get(fieldMeta, 'fieldType');
return (
<FastField name={comparatorFieldPath}>
{({ form, field }) => (
<FormGroup className={'form-group--comparator'}>
<AdvancedFilterCompatatorField
dataType={fieldType}
className={Classes.FILL}
selectedItem={field.value}
onItemSelect={(option) => {
form.setFieldValue(comparatorFieldPath, option.value);
}}
/>
</FormGroup>
)}
</FastField>
);
}
/**
* Changes default value of comparator field in the condition row once the
* field option changing.
*/
function useDefaultComparatorFieldValue({
getConditionValue,
setConditionValue,
fieldMeta,
}) {
const fieldKeyValue = getConditionValue('fieldKey');
const comparatorsOptions = React.useMemo(
() => getConditionTypeCompatators(fieldMeta.fieldType),
[fieldMeta.fieldType],
);
useUpdateEffect(() => {
if (fieldKeyValue) {
const defaultValue = get(first(comparatorsOptions), 'value');
setConditionValue('comparator', defaultValue);
}
}, [fieldKeyValue, setConditionValue, comparatorsOptions]);
}
/**
* Resource fields field.
*/
function FilterFieldsField() {
const {
getConditionFieldPath,
getConditionValue,
setConditionValue,
fieldMeta,
} = useFilterCondition();
const { fields } = useAdvancedFilterContext();
const fieldPath = getConditionFieldPath('fieldKey');
const valueFieldPath = getConditionFieldPath('value');
useDefaultComparatorFieldValue({
getConditionValue,
setConditionValue,
fieldMeta,
});
return (
<FastField name={fieldPath}>
{({ field, form }) => (
<FormGroup className={'form-group--fieldKey'}>
<ListSelect
selectedItem={field.value}
textProp={'label'}
selectedItemProp={'value'}
items={transformFieldsToOptions(fields)}
className={Classes.FILL}
onItemSelect={(option) => {
form.setFieldValue(fieldPath, option.value);
// Resets the value field to empty once the field option changing.
form.setFieldValue(valueFieldPath, '');
}}
popoverProps={{
inline: true,
minimal: true,
captureDismiss: true,
}}
/>
</FormGroup>
)}
</FastField>
);
}
/**
* Advanced filter value field.
*/
function FilterValueField() {
const { conditionIndex, fieldMeta, getConditionFieldPath } =
useFilterCondition();
// Can't continue if the given field key is not selected yet.
if (!fieldMeta) {
return null;
}
// Field meta type, name and options.
const fieldType = get(fieldMeta, 'fieldType');
const fieldName = get(fieldMeta, 'name');
const options = get(fieldMeta, 'options');
const valueFieldPath = getConditionFieldPath('value');
return (
<FastField
name={valueFieldPath}
fieldKey={fieldType} // Pass to shouldUpdate function.
shouldUpdate={shouldFilterValueFieldUpdate}
>
{({ form: { setFieldValue }, field }) => (
<FormGroup className={'form-group--value'}>
<AdvancedFilterValueField
isFocus={conditionIndex === 0}
value={field.value}
key={'name'}
label={fieldName}
fieldType={fieldType}
options={options}
onChange={(value) => {
setFieldValue(valueFieldPath, value);
}}
/>
</FormGroup>
)}
</FastField>
);
}
/**
* Advanced filter condition line.
*/
function AdvancedFilterDropdownCondition({ conditionIndex, onRemoveClick }) {
// Handle click remove condition.
const handleClickRemoveCondition = () => {
onRemoveClick && onRemoveClick(conditionIndex);
};
return (
<div className="filter-dropdown__condition">
<FilterConditionProvider conditionIndex={conditionIndex}>
<FilterConditionField />
<FilterFieldsField />
<FilterCompatatorFilter />
<FilterValueField />
<Button
icon={<Icon icon="times" iconSize={14} />}
minimal={true}
onClick={handleClickRemoveCondition}
className={'button--remove'}
/>
</FilterConditionProvider>
</div>
);
}
/**
* Advanced filter dropdown condition.
*/
function AdvancedFilterDropdownConditions({ push, remove, replace, form }) {
const { initialCondition } = useAdvancedFilterContext();
// Handle remove condition.
const handleClickRemoveCondition = (conditionIndex) => {
if (form.values.conditions.length > 1) {
remove(conditionIndex);
} else {
replace(0, { ...initialCondition });
}
};
// Handle new condition button click.
const handleNewConditionBtnClick = (index) => {
push({ ...initialCondition });
};
return (
<div className="filter-dropdonw__conditions-wrap">
<div className={'filter-dropdown__conditions'}>
{form.values.conditions.map((condition, index) => (
<AdvancedFilterDropdownCondition
conditionIndex={index}
onRemoveClick={handleClickRemoveCondition}
/>
))}
</div>
<AdvancedFilterDropdownFooter onClick={handleNewConditionBtnClick} />
</div>
);
}
/**
* Advanced filter dropdown form.
*/
function AdvancedFilterDropdownForm() {
// Advanced filter auto-save.
useAdvancedFilterAutoSubmit();
return (
<div className="filter-dropdown__form">
<FieldArray
name={'conditions'}
render={({ ...fieldArrayProps }) => (
<AdvancedFilterDropdownConditions {...fieldArrayProps} />
)}
/>
</div>
);
}
/**
* Advanced filter dropdown footer.
*/
function AdvancedFilterDropdownFooter({ onClick }) {
// Handle new filter condition button click.
const onClickNewFilter = (event) => {
onClick && onClick(event);
};
return (
<div className="filter-dropdown__footer">
<Button minimal={true} onClick={onClickNewFilter}>
<T id={'new_conditional'} />
</Button>
</div>
);
}
/**
* Advanced filter dropdown.
*/
export function AdvancedFilterDropdown({
fields,
conditions,
defaultFieldKey,
defaultComparator,
defaultValue,
defaultCondition,
onFilterChange,
}) {
// Initial condition.
const initialCondition = {
fieldKey: defaultFieldKey,
comparator: defaultTo(defaultComparator, 'contain'),
condition: defaultTo(defaultCondition, 'or'),
value: defaultTo(defaultValue, ''),
};
// Initial conditions.
const initialConditions = !isEmpty(conditions)
? conditions
: [initialCondition, initialCondition];
const [prevConditions, setPrevConditions] = React.useState(initialConditions);
// Handle the filter dropdown form submit.
const handleFitlerDropdownSubmit = (values) => {
const conditions = filterConditionRoles(values.conditions);
// Campare the current conditions with previous conditions, if they were equal
// there is no need to execute `onFilterChange` function.
if (!isEqual(prevConditions, conditions)) {
onFilterChange && onFilterChange(conditions);
setPrevConditions(conditions);
}
};
// Filter dropdown validation schema.
const validationSchema = getFilterDropdownSchema();
// Initial values.
const initialValues = {
conditions: initialConditions,
};
return (
<div className="filter-dropdown">
<AdvancedFilterDropdownProvider
initialCondition={initialCondition}
fields={fields}
>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
component={AdvancedFilterDropdownForm}
onSubmit={handleFitlerDropdownSubmit}
/>
</AdvancedFilterDropdownProvider>
</div>
);
}

View File

@@ -0,0 +1,397 @@
import React from 'react';
import { Formik, FastField, FieldArray, useFormikContext } from 'formik';
import {
Button,
FormGroup,
Classes,
InputGroup,
MenuItem,
} from '@blueprintjs/core';
import { get, first, defaultTo, isEqual, isEmpty } from 'lodash';
import intl from 'react-intl-universal';
import { Choose, Icon, FormattedMessage as T, ListSelect } from '@/components';
import { useUpdateEffect } from '@/hooks';
import {
AdvancedFilterDropdownProvider,
FilterConditionProvider,
useFilterCondition,
useAdvancedFilterContext,
} from './AdvancedFilterDropdownContext';
import AdvancedFilterCompatatorField from './AdvancedFilterCompatatorField';
import AdvancedFilterValueField from './AdvancedFilterValueField';
import {
filterConditionRoles,
getConditionalsOptions,
transformFieldsToOptions,
shouldFilterValueFieldUpdate,
getConditionTypeCompatators,
} from './utils';
import { getFilterDropdownSchema } from './AdvancedFilter.schema';
import { useAdvancedFilterAutoSubmit } from './components';
/**
* Condition item list renderer.
*/
function ConditionItemRenderer(condition, { handleClick, modifiers, query }) {
return (
<MenuItem
text={
<>
<div>{condition.label}</div>
<span className="text-hint">{condition.text}</span>
</>
}
key={condition.value}
onClick={handleClick}
/>
);
}
/**
* Filter condition field.
*/
function FilterConditionField() {
const conditionalsOptions = getConditionalsOptions();
const { conditionIndex, getConditionFieldPath } = useFilterCondition();
const conditionFieldPath = getConditionFieldPath('condition');
return (
<FastField name={conditionFieldPath}>
{({ form, field }) => (
<FormGroup className={'form-group--condition'}>
<Choose>
<Choose.When condition={conditionIndex === 0}>
<InputGroup disabled value={intl.get('filter.when')} />
</Choose.When>
<Choose.Otherwise>
<ListSelect
selectedItem={field.value}
textProp={'label'}
selectedItemProp={'value'}
labelProp={'text'}
items={conditionalsOptions}
className={Classes.FILL}
filterable={false}
onItemSelect={(option) => {
form.setFieldValue(conditionFieldPath, option.value);
}}
popoverProps={{
inline: true,
minimal: true,
captureDismiss: true,
}}
itemRenderer={ConditionItemRenderer}
/>
</Choose.Otherwise>
</Choose>
</FormGroup>
)}
</FastField>
);
}
/**
* Compatator field.
*/
function FilterCompatatorFilter() {
const { getConditionFieldPath, fieldMeta } = useFilterCondition();
const comparatorFieldPath = getConditionFieldPath('comparator');
const fieldType = get(fieldMeta, 'fieldType');
return (
<FastField name={comparatorFieldPath}>
{({ form, field }) => (
<FormGroup className={'form-group--comparator'}>
<AdvancedFilterCompatatorField
dataType={fieldType}
className={Classes.FILL}
selectedItem={field.value}
onItemSelect={(option) => {
form.setFieldValue(comparatorFieldPath, option.value);
}}
/>
</FormGroup>
)}
</FastField>
);
}
/**
* Changes default value of comparator field in the condition row once the
* field option changing.
*/
function useDefaultComparatorFieldValue({
getConditionValue,
setConditionValue,
fieldMeta,
}) {
const fieldKeyValue = getConditionValue('fieldKey');
const comparatorsOptions = React.useMemo(
() => getConditionTypeCompatators(fieldMeta.fieldType),
[fieldMeta.fieldType],
);
useUpdateEffect(() => {
if (fieldKeyValue) {
const defaultValue = get(first(comparatorsOptions), 'value');
setConditionValue('comparator', defaultValue);
}
}, [fieldKeyValue, setConditionValue, comparatorsOptions]);
}
/**
* Resource fields field.
*/
function FilterFieldsField() {
const {
getConditionFieldPath,
getConditionValue,
setConditionValue,
fieldMeta,
} = useFilterCondition();
const { fields } = useAdvancedFilterContext();
const fieldPath = getConditionFieldPath('fieldKey');
const valueFieldPath = getConditionFieldPath('value');
useDefaultComparatorFieldValue({
getConditionValue,
setConditionValue,
fieldMeta,
});
return (
<FastField name={fieldPath}>
{({ field, form }) => (
<FormGroup className={'form-group--fieldKey'}>
<ListSelect
selectedItem={field.value}
textProp={'label'}
selectedItemProp={'value'}
items={transformFieldsToOptions(fields)}
className={Classes.FILL}
onItemSelect={(option) => {
form.setFieldValue(fieldPath, option.value);
// Resets the value field to empty once the field option changing.
form.setFieldValue(valueFieldPath, '');
}}
popoverProps={{
inline: true,
minimal: true,
captureDismiss: true,
}}
/>
</FormGroup>
)}
</FastField>
);
}
/**
* Advanced filter value field.
*/
function FilterValueField() {
const { conditionIndex, fieldMeta, getConditionFieldPath } =
useFilterCondition();
// Can't continue if the given field key is not selected yet.
if (!fieldMeta) {
return null;
}
// Field meta type, name and options.
const fieldType = get(fieldMeta, 'fieldType');
const fieldName = get(fieldMeta, 'name');
const options = get(fieldMeta, 'options');
const valueFieldPath = getConditionFieldPath('value');
return (
<FastField
name={valueFieldPath}
fieldKey={fieldType} // Pass to shouldUpdate function.
shouldUpdate={shouldFilterValueFieldUpdate}
>
{({ form: { setFieldValue }, field }) => (
<FormGroup className={'form-group--value'}>
<AdvancedFilterValueField
isFocus={conditionIndex === 0}
value={field.value}
key={'name'}
label={fieldName}
fieldType={fieldType}
options={options}
onChange={(value) => {
setFieldValue(valueFieldPath, value);
}}
/>
</FormGroup>
)}
</FastField>
);
}
/**
* Advanced filter condition line.
*/
function AdvancedFilterDropdownCondition({ conditionIndex, onRemoveClick }) {
// Handle click remove condition.
const handleClickRemoveCondition = () => {
onRemoveClick && onRemoveClick(conditionIndex);
};
return (
<div className="filter-dropdown__condition">
<FilterConditionProvider conditionIndex={conditionIndex}>
<FilterConditionField />
<FilterFieldsField />
<FilterCompatatorFilter />
<FilterValueField />
<Button
icon={<Icon icon="times" iconSize={14} />}
minimal={true}
onClick={handleClickRemoveCondition}
className={'button--remove'}
/>
</FilterConditionProvider>
</div>
);
}
/**
* Advanced filter dropdown condition.
*/
function AdvancedFilterDropdownConditions({ push, remove, replace, form }) {
const { initialCondition } = useAdvancedFilterContext();
// Handle remove condition.
const handleClickRemoveCondition = (conditionIndex) => {
if (form.values.conditions.length > 1) {
remove(conditionIndex);
} else {
replace(0, { ...initialCondition });
}
};
// Handle new condition button click.
const handleNewConditionBtnClick = (index) => {
push({ ...initialCondition });
};
return (
<div className="filter-dropdonw__conditions-wrap">
<div className={'filter-dropdown__conditions'}>
{form.values.conditions.map((condition, index) => (
<AdvancedFilterDropdownCondition
conditionIndex={index}
onRemoveClick={handleClickRemoveCondition}
/>
))}
</div>
<AdvancedFilterDropdownFooter onClick={handleNewConditionBtnClick} />
</div>
);
}
/**
* Advanced filter dropdown form.
*/
function AdvancedFilterDropdownForm() {
// Advanced filter auto-save.
useAdvancedFilterAutoSubmit();
return (
<div className="filter-dropdown__form">
<FieldArray
name={'conditions'}
render={({ ...fieldArrayProps }) => (
<AdvancedFilterDropdownConditions {...fieldArrayProps} />
)}
/>
</div>
);
}
/**
* Advanced filter dropdown footer.
*/
function AdvancedFilterDropdownFooter({ onClick }) {
// Handle new filter condition button click.
const onClickNewFilter = (event) => {
onClick && onClick(event);
};
return (
<div className="filter-dropdown__footer">
<Button minimal={true} onClick={onClickNewFilter}>
<T id={'new_conditional'} />
</Button>
</div>
);
}
/**
* Advanced filter dropdown.
*/
export function AdvancedFilterDropdown({
fields,
conditions,
defaultFieldKey,
defaultComparator,
defaultValue,
defaultCondition,
onFilterChange,
}) {
// Initial condition.
const initialCondition = {
fieldKey: defaultFieldKey,
comparator: defaultTo(defaultComparator, 'contain'),
condition: defaultTo(defaultCondition, 'or'),
value: defaultTo(defaultValue, ''),
};
// Initial conditions.
const initialConditions = !isEmpty(conditions)
? conditions
: [initialCondition, initialCondition];
const [prevConditions, setPrevConditions] = React.useState(initialConditions);
// Handle the filter dropdown form submit.
const handleFitlerDropdownSubmit = (values) => {
const conditions = filterConditionRoles(values.conditions);
// Campare the current conditions with previous conditions, if they were equal
// there is no need to execute `onFilterChange` function.
if (!isEqual(prevConditions, conditions)) {
onFilterChange && onFilterChange(conditions);
setPrevConditions(conditions);
}
};
// Filter dropdown validation schema.
const validationSchema = getFilterDropdownSchema();
// Initial values.
const initialValues = {
conditions: initialConditions,
};
return (
<div className="filter-dropdown">
<AdvancedFilterDropdownProvider
initialCondition={initialCondition}
fields={fields}
>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
component={AdvancedFilterDropdownForm}
onSubmit={handleFitlerDropdownSubmit}
/>
</AdvancedFilterDropdownProvider>
</div>
);
}

View File

@@ -1,131 +0,0 @@
import React from 'react';
import { Position, Checkbox, InputGroup } from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import moment from 'moment';
import intl from 'react-intl-universal';
import { isUndefined } from 'lodash';
import { useAutofocus } from 'hooks';
import { T, Choose, ListSelect } from 'components';
import { momentFormatter } from 'utils';
function AdvancedFilterEnumerationField({ options, value, ...rest }) {
return (
<ListSelect
items={options}
selectedItem={value}
popoverProps={{
fill: true,
inline: true,
minimal: true,
captureDismiss: true,
}}
defaultText={<T id={'filter.select_option'} />}
textProp={'label'}
selectedItemProp={'key'}
{...rest}
/>
);
}
const IFieldType = {
ENUMERATION: 'enumeration',
BOOLEAN: 'boolean',
NUMBER: 'number',
DATE: 'date',
};
function tansformDateValue(date, defaultValue = null) {
return date ? moment(date).toDate() : defaultValue;
}
/**
* Advanced filter value field detarminer.
*/
export default function AdvancedFilterValueField2({
value,
fieldType,
options,
onChange,
isFocus,
}) {
const [localValue, setLocalValue] = React.useState(value);
React.useEffect(() => {
if (localValue !== value && !isUndefined(value)) {
setLocalValue(value);
}
}, [localValue, value]);
// Input field reference.
const valueRef = useAutofocus(isFocus);
const triggerOnChange = (value) => onChange && onChange(value);
// Handle input change.
const handleInputChange = (e) => {
if (e.currentTarget.type === 'checkbox') {
setLocalValue(e.currentTarget.checked);
triggerOnChange(e.currentTarget.checked);
} else {
setLocalValue(e.currentTarget.value);
triggerOnChange(e.currentTarget.value);
}
};
// Handle enumeration field type change.
const handleEnumerationChange = (option) => {
setLocalValue(option.key);
triggerOnChange(option.key);
};
// Handle date field change.
const handleDateChange = (date) => {
const formattedDate = moment(date).format('YYYY/MM/DD');
setLocalValue(formattedDate);
triggerOnChange(formattedDate);
};
return (
<Choose>
<Choose.When condition={fieldType === IFieldType.ENUMERATION}>
<AdvancedFilterEnumerationField
options={options}
value={localValue}
onItemSelect={handleEnumerationChange}
/>
</Choose.When>
<Choose.When condition={fieldType === IFieldType.DATE}>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(localValue)}
onChange={handleDateChange}
popoverProps={{
minimal: true,
position: Position.BOTTOM,
}}
shortcuts={true}
placeholder={intl.get('filter.enter_date')}
fill={true}
inputProps={{
fill: true,
}}
/>
</Choose.When>
<Choose.When condition={fieldType === IFieldType.BOOLEAN}>
<Checkbox value={localValue} onChange={handleInputChange} />
</Choose.When>
<Choose.Otherwise>
<InputGroup
placeholder={intl.get('filter.value')}
onChange={handleInputChange}
value={localValue}
inputRef={valueRef}
/>
</Choose.Otherwise>
</Choose>
);
}

View File

@@ -0,0 +1,131 @@
import React from 'react';
import { Position, Checkbox, InputGroup } from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import moment from 'moment';
import intl from 'react-intl-universal';
import { isUndefined } from 'lodash';
import { useAutofocus } from '@/hooks';
import { T, Choose, ListSelect } from '@/components';
import { momentFormatter } from '@/utils';
function AdvancedFilterEnumerationField({ options, value, ...rest }) {
return (
<ListSelect
items={options}
selectedItem={value}
popoverProps={{
fill: true,
inline: true,
minimal: true,
captureDismiss: true,
}}
defaultText={<T id={'filter.select_option'} />}
textProp={'label'}
selectedItemProp={'key'}
{...rest}
/>
);
}
const IFieldType = {
ENUMERATION: 'enumeration',
BOOLEAN: 'boolean',
NUMBER: 'number',
DATE: 'date',
};
function tansformDateValue(date, defaultValue = null) {
return date ? moment(date).toDate() : defaultValue;
}
/**
* Advanced filter value field detarminer.
*/
export default function AdvancedFilterValueField2({
value,
fieldType,
options,
onChange,
isFocus,
}) {
const [localValue, setLocalValue] = React.useState(value);
React.useEffect(() => {
if (localValue !== value && !isUndefined(value)) {
setLocalValue(value);
}
}, [localValue, value]);
// Input field reference.
const valueRef = useAutofocus(isFocus);
const triggerOnChange = (value) => onChange && onChange(value);
// Handle input change.
const handleInputChange = (e) => {
if (e.currentTarget.type === 'checkbox') {
setLocalValue(e.currentTarget.checked);
triggerOnChange(e.currentTarget.checked);
} else {
setLocalValue(e.currentTarget.value);
triggerOnChange(e.currentTarget.value);
}
};
// Handle enumeration field type change.
const handleEnumerationChange = (option) => {
setLocalValue(option.key);
triggerOnChange(option.key);
};
// Handle date field change.
const handleDateChange = (date) => {
const formattedDate = moment(date).format('YYYY/MM/DD');
setLocalValue(formattedDate);
triggerOnChange(formattedDate);
};
return (
<Choose>
<Choose.When condition={fieldType === IFieldType.ENUMERATION}>
<AdvancedFilterEnumerationField
options={options}
value={localValue}
onItemSelect={handleEnumerationChange}
/>
</Choose.When>
<Choose.When condition={fieldType === IFieldType.DATE}>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(localValue)}
onChange={handleDateChange}
popoverProps={{
minimal: true,
position: Position.BOTTOM,
}}
shortcuts={true}
placeholder={intl.get('filter.enter_date')}
fill={true}
inputProps={{
fill: true,
}}
/>
</Choose.When>
<Choose.When condition={fieldType === IFieldType.BOOLEAN}>
<Checkbox value={localValue} onChange={handleInputChange} />
</Choose.When>
<Choose.Otherwise>
<InputGroup
placeholder={intl.get('filter.value')}
onChange={handleInputChange}
value={localValue}
inputRef={valueRef}
/>
</Choose.Otherwise>
</Choose>
);
}

View File

@@ -1,112 +0,0 @@
import intl from 'react-intl-universal';
import {
defaultFastFieldShouldUpdate,
uniqueMultiProps,
checkRequiredProperties,
} from 'utils';
// Conditions options.
export const getConditionalsOptions = () => [
{
value: 'and',
label: intl.get('and'),
text: intl.get('filter.all_filters_must_match'),
},
{
value: 'or',
label: intl.get('or'),
text: intl.get('filter.atleast_one_filter_must_match'),
},
];
export const getBooleanCompatators = () => [
{ value: 'is', label: intl.get('is') },
{ value: 'is_not', label: intl.get('is_not') },
];
export const getTextCompatators = () => [
{ value: 'contain', label: intl.get('contain') },
{ value: 'not_contain', label: intl.get('not_contain') },
{ value: 'equal', label: intl.get('equals') },
{ value: 'not_equal', label: intl.get('not_equals') },
{ value: 'starts_with', label: intl.get('starts_with') },
{ value: 'ends_with', label: intl.get('ends_with') },
];
export const getDateCompatators = () => [
{ value: 'in', label: intl.get('in') },
{ value: 'after', label: intl.get('after') },
{ value: 'before', label: intl.get('before') },
];
export const getOptionsCompatators = () => [
{ value: 'is', label: intl.get('is') },
{ value: 'is_not', label: intl.get('is_not') },
];
export const getNumberCampatators = () => [
{ value: 'equal', label: intl.get('equals') },
{ value: 'not_equal', label: intl.get('not_equal') },
{ value: 'bigger_than', label: intl.get('bigger_than') },
{ value: 'bigger_or_equal', label: intl.get('bigger_or_equals') },
{ value: 'smaller_than', label: intl.get('smaller_than') },
{ value: 'smaller_or_equal', label: intl.get('smaller_or_equals') },
];
export const getConditionTypeCompatators = (
dataType,
) => {
return [
...(dataType === 'enumeration'
? [...getOptionsCompatators()]
: dataType === 'date'
? [...getDateCompatators()]
: dataType === 'boolean'
? [...getBooleanCompatators()]
: dataType === 'number'
? [...getNumberCampatators()]
: [...getTextCompatators()]),
];
};
export const getConditionDefaultCompatator = (
dataType,
) => {
const compatators = getConditionTypeCompatators(dataType);
return compatators[0];
};
export const transformFieldsToOptions = (fields) =>
fields.map((field) => ({
value: field.key,
label: field.name,
}));
/**
* Filtered conditions that don't contain atleast on required fields or
* fileds keys that not exists.
* @param {IFilterRole[]} conditions
* @returns
*/
export const filterConditionRoles = (
conditions,
) => {
const requiredProps = ['fieldKey', 'condition', 'comparator', 'value'];
const filteredConditions = conditions.filter(
(condition) =>
!checkRequiredProperties(condition, requiredProps),
);
return uniqueMultiProps(filteredConditions, requiredProps);
};
/**
* Detarmines the value field when should update.
* @returns {boolean}
*/
export const shouldFilterValueFieldUpdate = (newProps, oldProps) => {
return (
newProps.fieldKey !== oldProps.fieldKey ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};

View File

@@ -0,0 +1,112 @@
import intl from 'react-intl-universal';
import {
defaultFastFieldShouldUpdate,
uniqueMultiProps,
checkRequiredProperties,
} from '@/utils';
// Conditions options.
export const getConditionalsOptions = () => [
{
value: 'and',
label: intl.get('and'),
text: intl.get('filter.all_filters_must_match'),
},
{
value: 'or',
label: intl.get('or'),
text: intl.get('filter.atleast_one_filter_must_match'),
},
];
export const getBooleanCompatators = () => [
{ value: 'is', label: intl.get('is') },
{ value: 'is_not', label: intl.get('is_not') },
];
export const getTextCompatators = () => [
{ value: 'contain', label: intl.get('contain') },
{ value: 'not_contain', label: intl.get('not_contain') },
{ value: 'equal', label: intl.get('equals') },
{ value: 'not_equal', label: intl.get('not_equals') },
{ value: 'starts_with', label: intl.get('starts_with') },
{ value: 'ends_with', label: intl.get('ends_with') },
];
export const getDateCompatators = () => [
{ value: 'in', label: intl.get('in') },
{ value: 'after', label: intl.get('after') },
{ value: 'before', label: intl.get('before') },
];
export const getOptionsCompatators = () => [
{ value: 'is', label: intl.get('is') },
{ value: 'is_not', label: intl.get('is_not') },
];
export const getNumberCampatators = () => [
{ value: 'equal', label: intl.get('equals') },
{ value: 'not_equal', label: intl.get('not_equal') },
{ value: 'bigger_than', label: intl.get('bigger_than') },
{ value: 'bigger_or_equal', label: intl.get('bigger_or_equals') },
{ value: 'smaller_than', label: intl.get('smaller_than') },
{ value: 'smaller_or_equal', label: intl.get('smaller_or_equals') },
];
export const getConditionTypeCompatators = (
dataType,
) => {
return [
...(dataType === 'enumeration'
? [...getOptionsCompatators()]
: dataType === 'date'
? [...getDateCompatators()]
: dataType === 'boolean'
? [...getBooleanCompatators()]
: dataType === 'number'
? [...getNumberCampatators()]
: [...getTextCompatators()]),
];
};
export const getConditionDefaultCompatator = (
dataType,
) => {
const compatators = getConditionTypeCompatators(dataType);
return compatators[0];
};
export const transformFieldsToOptions = (fields) =>
fields.map((field) => ({
value: field.key,
label: field.name,
}));
/**
* Filtered conditions that don't contain atleast on required fields or
* fileds keys that not exists.
* @param {IFilterRole[]} conditions
* @returns
*/
export const filterConditionRoles = (
conditions,
) => {
const requiredProps = ['fieldKey', 'condition', 'comparator', 'value'];
const filteredConditions = conditions.filter(
(condition) =>
!checkRequiredProperties(condition, requiredProps),
);
return uniqueMultiProps(filteredConditions, requiredProps);
};
/**
* Detarmines the value field when should update.
* @returns {boolean}
*/
export const shouldFilterValueFieldUpdate = (newProps, oldProps) => {
return (
newProps.fieldKey !== oldProps.fieldKey ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};

View File

@@ -1,63 +0,0 @@
import React from 'react';
import { Router, Switch, Route } from 'react-router';
import { createBrowserHistory } from 'history';
import { QueryClientProvider, QueryClient } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';
import 'style/App.scss';
import 'moment/locale/ar-ly';
import 'moment/locale/es-us';
import AppIntlLoader from './AppIntlLoader';
import PrivateRoute from 'components/Guards/PrivateRoute';
import GlobalErrors from 'containers/GlobalErrors/GlobalErrors';
import DashboardPrivatePages from 'components/Dashboard/PrivatePages';
import Authentication from 'components/Authentication';
import { SplashScreen, DashboardThemeProvider } from '../components';
import { queryConfig } from '../hooks/query/base';
/**
* App inner.
*/
function AppInsider({ history }) {
return (
<div className="App">
<DashboardThemeProvider>
<Router history={history}>
<Switch>
<Route path={'/auth'} component={Authentication} />
<Route path={'/'}>
<PrivateRoute component={DashboardPrivatePages} />
</Route>
</Switch>
</Router>
<GlobalErrors />
</DashboardThemeProvider>
</div>
);
}
/**
* Core application.
*/
export default function App() {
// Browser history.
const history = createBrowserHistory();
// Query client.
const queryClient = new QueryClient(queryConfig);
return (
<QueryClientProvider client={queryClient}>
<SplashScreen />
<AppIntlLoader>
<AppInsider history={history} />
</AppIntlLoader>
<ReactQueryDevtools initialIsOpen />
</QueryClientProvider>
);
}

63
src/components/App.tsx Normal file
View File

@@ -0,0 +1,63 @@
import React from 'react';
import { Router, Switch, Route } from 'react-router';
import { createBrowserHistory } from 'history';
import { QueryClientProvider, QueryClient } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';
import '@/style/App.scss';
import 'moment/locale/ar-ly';
import 'moment/locale/es-us';
import AppIntlLoader from './AppIntlLoader';
import PrivateRoute from '@/components/Guards/PrivateRoute';
import GlobalErrors from '@/containers/GlobalErrors/GlobalErrors';
import DashboardPrivatePages from '@/components/Dashboard/PrivatePages';
import Authentication from '@/components/Authentication';
import { SplashScreen, DashboardThemeProvider } from '../components';
import { queryConfig } from '../hooks/query/base';
/**
* App inner.
*/
function AppInsider({ history }) {
return (
<div className="App">
<DashboardThemeProvider>
<Router history={history}>
<Switch>
<Route path={'/auth'} component={Authentication} />
<Route path={'/'}>
<PrivateRoute component={DashboardPrivatePages} />
</Route>
</Switch>
</Router>
<GlobalErrors />
</DashboardThemeProvider>
</div>
);
}
/**
* Core application.
*/
export default function App() {
// Browser history.
const history = createBrowserHistory();
// Query client.
const queryClient = new QueryClient(queryConfig);
return (
<QueryClientProvider client={queryClient}>
<SplashScreen />
<AppIntlLoader>
<AppInsider history={history} />
</AppIntlLoader>
<ReactQueryDevtools initialIsOpen />
</QueryClientProvider>
);
}

View File

@@ -1,156 +0,0 @@
import React from 'react';
import moment from 'moment';
import { setLocale } from 'yup';
import intl from 'react-intl-universal';
import { find } from 'lodash';
import rtlDetect from 'rtl-detect';
import * as R from 'ramda';
import { AppIntlProvider } from './AppIntlProvider';
import { useSplashLoading } from '../hooks/state';
import { useWatchImmediate } from '../hooks';
import withDashboardActions from '../containers/Dashboard/withDashboardActions';
const SUPPORTED_LOCALES = [
{ name: 'English', value: 'en' },
{ name: 'العربية', value: 'ar' },
];
/**
* Retrieve the current local.
*/
function getCurrentLocal() {
let currentLocale = intl.determineLocale({
urlLocaleKey: 'lang',
cookieLocaleKey: 'locale',
localStorageLocaleKey: 'lang',
});
if (!find(SUPPORTED_LOCALES, { value: currentLocale })) {
currentLocale = 'en';
}
return currentLocale;
}
/**
* Loads the localization data of the given locale.
*/
function loadLocales(currentLocale) {
return import(`../lang/${currentLocale}/index.json`);
}
/**
* Loads the localization data of yup validation library.
*/
function loadYupLocales(currentLocale) {
return import(`../lang/${currentLocale}/locale`);
}
/**
* Modifies the html document direction to RTl if it was rtl-language.
*/
function useDocumentDirectionModifier(locale, isRTL) {
React.useEffect(() => {
if (isRTL) {
const htmlDocument = document.querySelector('html');
htmlDocument.setAttribute('dir', 'rtl');
htmlDocument.setAttribute('lang', locale);
}
}, [isRTL, locale]);
}
function transformMomentLocale(currentLocale) {
return currentLocale === 'ar' ? 'ar-ly' : currentLocale;
}
/**
* Loads application locales of the given current locale.
* @param {string} currentLocale
* @returns {{ isLoading: boolean }}
*/
function useAppLoadLocales(currentLocale) {
const [startLoading, stopLoading] = useSplashLoading();
const [isLoading, setIsLoading] = React.useState(true);
React.useEffect(() => {
// Lodas the locales data file.
loadLocales(currentLocale)
.then((results) => {
return intl.init({
currentLocale,
locales: {
[currentLocale]: results,
},
});
})
.then(() => {
moment.locale(transformMomentLocale(currentLocale));
setIsLoading(false);
});
}, [currentLocale, stopLoading]);
// Watches the value to start/stop splash screen.
useWatchImmediate(
(value) => (value ? startLoading() : stopLoading()),
isLoading,
);
return { isLoading };
}
/**
* Loads application yup locales based on the given current locale.
* @param {string} currentLocale
* @returns {{ isLoading: boolean }}
*/
function useAppYupLoadLocales(currentLocale) {
const [startLoading, stopLoading] = useSplashLoading();
const [isLoading, setIsLoading] = React.useState(true);
React.useEffect(() => {
loadYupLocales(currentLocale)
.then(({ locale }) => {
setLocale(locale);
setIsLoading(false);
})
.then(() => {});
}, [currentLocale, stopLoading]);
// Watches the valiue to start/stop splash screen.
useWatchImmediate(
(value) => (value ? startLoading() : stopLoading()),
isLoading,
);
return { isLoading };
}
/**
* Application Intl loader.
*/
function AppIntlLoader({ children }) {
// Retrieve the current locale.
const currentLocale = getCurrentLocal();
// Detarmines the document direction based on the given locale.
const isRTL = rtlDetect.isRtlLang(currentLocale);
// Modifies the html document direction
useDocumentDirectionModifier(currentLocale, isRTL);
// Loads yup localization of the given locale.
const { isLoading: isAppYupLocalesLoading } =
useAppYupLoadLocales(currentLocale);
// Loads application locales of the given locale.
const { isLoading: isAppLocalesLoading } = useAppLoadLocales(currentLocale);
// Detarmines whether the app locales loading.
const isLoading = isAppYupLocalesLoading || isAppLocalesLoading;
return (
<AppIntlProvider currentLocale={currentLocale} isRTL={isRTL}>
{isLoading ? null : children}
</AppIntlProvider>
);
}
export default R.compose(withDashboardActions)(AppIntlLoader);

View File

@@ -0,0 +1,156 @@
import React from 'react';
import moment from 'moment';
import { setLocale } from 'yup';
import intl from 'react-intl-universal';
import { find } from 'lodash';
import rtlDetect from 'rtl-detect';
import * as R from 'ramda';
import { AppIntlProvider } from './AppIntlProvider';
import { useSplashLoading } from '@/hooks/state';
import { useWatchImmediate } from '../hooks';
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
const SUPPORTED_LOCALES = [
{ name: 'English', value: 'en' },
{ name: 'العربية', value: 'ar' },
];
/**
* Retrieve the current local.
*/
function getCurrentLocal() {
let currentLocale = intl.determineLocale({
urlLocaleKey: 'lang',
cookieLocaleKey: 'locale',
localStorageLocaleKey: 'lang',
});
if (!find(SUPPORTED_LOCALES, { value: currentLocale })) {
currentLocale = 'en';
}
return currentLocale;
}
/**
* Loads the localization data of the given locale.
*/
function loadLocales(currentLocale) {
return import(`../lang/${currentLocale}/index.json`);
}
/**
* Loads the localization data of yup validation library.
*/
function loadYupLocales(currentLocale) {
return import(`../lang/${currentLocale}/locale`);
}
/**
* Modifies the html document direction to RTl if it was rtl-language.
*/
function useDocumentDirectionModifier(locale, isRTL) {
React.useEffect(() => {
if (isRTL) {
const htmlDocument = document.querySelector('html');
htmlDocument.setAttribute('dir', 'rtl');
htmlDocument.setAttribute('lang', locale);
}
}, [isRTL, locale]);
}
function transformMomentLocale(currentLocale) {
return currentLocale === 'ar' ? 'ar-ly' : currentLocale;
}
/**
* Loads application locales of the given current locale.
* @param {string} currentLocale
* @returns {{ isLoading: boolean }}
*/
function useAppLoadLocales(currentLocale) {
const [startLoading, stopLoading] = useSplashLoading();
const [isLoading, setIsLoading] = React.useState(true);
React.useEffect(() => {
// Lodas the locales data file.
loadLocales(currentLocale)
.then((results) => {
return intl.init({
currentLocale,
locales: {
[currentLocale]: results,
},
});
})
.then(() => {
moment.locale(transformMomentLocale(currentLocale));
setIsLoading(false);
});
}, [currentLocale, stopLoading]);
// Watches the value to start/stop splash screen.
useWatchImmediate(
(value) => (value ? startLoading() : stopLoading()),
isLoading,
);
return { isLoading };
}
/**
* Loads application yup locales based on the given current locale.
* @param {string} currentLocale
* @returns {{ isLoading: boolean }}
*/
function useAppYupLoadLocales(currentLocale) {
const [startLoading, stopLoading] = useSplashLoading();
const [isLoading, setIsLoading] = React.useState(true);
React.useEffect(() => {
loadYupLocales(currentLocale)
.then(({ locale }) => {
setLocale(locale);
setIsLoading(false);
})
.then(() => {});
}, [currentLocale, stopLoading]);
// Watches the valiue to start/stop splash screen.
useWatchImmediate(
(value) => (value ? startLoading() : stopLoading()),
isLoading,
);
return { isLoading };
}
/**
* Application Intl loader.
*/
function AppIntlLoader({ children }) {
// Retrieve the current locale.
const currentLocale = getCurrentLocal();
// Detarmines the document direction based on the given locale.
const isRTL = rtlDetect.isRtlLang(currentLocale);
// Modifies the html document direction
useDocumentDirectionModifier(currentLocale, isRTL);
// Loads yup localization of the given locale.
const { isLoading: isAppYupLocalesLoading } =
useAppYupLoadLocales(currentLocale);
// Loads application locales of the given locale.
const { isLoading: isAppLocalesLoading } = useAppLoadLocales(currentLocale);
// Detarmines whether the app locales loading.
const isLoading = isAppYupLocalesLoading || isAppLocalesLoading;
return (
<AppIntlProvider currentLocale={currentLocale} isRTL={isRTL}>
{isLoading ? null : children}
</AppIntlProvider>
);
}
export default R.compose(withDashboardActions)(AppIntlLoader);

View File

@@ -1,8 +0,0 @@
import { Position, Toaster, Intent } from "@blueprintjs/core";
const AppToaster = Toaster.create({
position: Position.RIGHT_BOTTOM,
intent: Intent.WARNING,
});
export default AppToaster;

View File

@@ -0,0 +1,6 @@
import { Position, Toaster, Intent } from '@blueprintjs/core';
export const AppToaster = Toaster.create({
position: Position.RIGHT_BOTTOM,
intent: Intent.WARNING,
});

View File

@@ -1,62 +0,0 @@
import React from 'react';
import { Redirect, Route, Switch, Link, useLocation } from 'react-router-dom';
import BodyClassName from 'react-body-classname';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import authenticationRoutes from 'routes/authentication';
import { FormattedMessage as T } from 'components';
import Icon from 'components/Icon';
import { useIsAuthenticated } from 'hooks/state';
import 'style/pages/Authentication/Auth.scss';
function PageFade(props) {
return <CSSTransition {...props} classNames="authTransition" timeout={500} />;
}
export default function AuthenticationWrapper({ ...rest }) {
const to = { pathname: '/' };
const location = useLocation();
const isAuthenticated = useIsAuthenticated();
const locationKey = location.pathname;
return (
<>
{isAuthenticated ? (
<Redirect to={to} />
) : (
<BodyClassName className={'authentication'}>
<div class="authentication-page">
<a
href={'http://bigcapital.ly'}
className={'authentication-page__goto-bigcapital'}
>
<T id={'go_to_bigcapital_com'} />
</a>
<div class="authentication-page__form-wrapper">
<div class="authentication-insider">
<div className={'authentication-insider__logo-section'}>
<Icon icon="bigcapital" height={37} width={214} />
</div>
<TransitionGroup>
<PageFade key={locationKey}>
<Switch>
{authenticationRoutes.map((route, index) => (
<Route
key={index}
path={route.path}
exact={route.exact}
component={route.component}
/>
))}
</Switch>
</PageFade>
</TransitionGroup>
</div>
</div>
</div>
</BodyClassName>
)}
</>
);
}

View File

@@ -0,0 +1,61 @@
import React from 'react';
import { Redirect, Route, Switch, Link, useLocation } from 'react-router-dom';
import BodyClassName from 'react-body-classname';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import authenticationRoutes from '@/routes/authentication';
import { Icon, FormattedMessage as T } from '@/components';
import { useIsAuthenticated } from '@/hooks/state';
import '@/style/pages/Authentication/Auth.scss';
function PageFade(props) {
return <CSSTransition {...props} classNames="authTransition" timeout={500} />;
}
export default function AuthenticationWrapper({ ...rest }) {
const to = { pathname: '/' };
const location = useLocation();
const isAuthenticated = useIsAuthenticated();
const locationKey = location.pathname;
return (
<>
{isAuthenticated ? (
<Redirect to={to} />
) : (
<BodyClassName className={'authentication'}>
<div class="authentication-page">
<a
href={'http://bigcapital.ly'}
className={'authentication-page__goto-bigcapital'}
>
<T id={'go_to_bigcapital_com'} />
</a>
<div class="authentication-page__form-wrapper">
<div class="authentication-insider">
<div className={'authentication-insider__logo-section'}>
<Icon icon="bigcapital" height={37} width={214} />
</div>
<TransitionGroup>
<PageFade key={locationKey}>
<Switch>
{authenticationRoutes.map((route, index) => (
<Route
key={index}
path={route.path}
exact={route.exact}
component={route.component}
/>
))}
</Switch>
</PageFade>
</TransitionGroup>
</div>
</div>
</div>
</BodyClassName>
)}
</>
);
}

View File

@@ -1,10 +0,0 @@
import React from 'react';
import { firstLettersArgs } from 'utils';
export default function AvatarCell({ row: { original }, size }) {
return (
<span className="avatar" data-size={size}>
{firstLettersArgs(original?.display_name)}
</span>
);
}

View File

@@ -0,0 +1,10 @@
import React from 'react';
import { firstLettersArgs } from '@/utils';
export default function AvatarCell({ row: { original }, size }) {
return (
<span className="avatar" data-size={size}>
{firstLettersArgs(original?.display_name)}
</span>
);
}

View File

@@ -1,205 +0,0 @@
import React from 'react';
import intl from 'react-intl-universal';
import styled from 'styled-components';
import { Classes } from '@blueprintjs/core';
import clsx from 'classnames';
import Icon from '../Icon';
const ACCOUNT_TYPE = {
CASH: 'cash',
BANK: 'bank',
CREDIT_CARD: 'credit-card',
};
const ACCOUNT_TYPE_PAIR_ICON = {
[ACCOUNT_TYPE.CASH]: 'payments',
[ACCOUNT_TYPE.CREDIT_CARD]: 'credit-card',
[ACCOUNT_TYPE.BANK]: 'account-balance',
};
function BankAccountMetaLine({ title, value, className }) {
return (
<MetaLineWrap className={className}>
<MetaLineTitle>{title}</MetaLineTitle>
{value && <MetaLineValue>{value}</MetaLineValue>}
</MetaLineWrap>
);
}
function BankAccountBalance({ amount, loading }) {
return (
<BankAccountBalanceWrap>
<BankAccountBalanceAmount
className={clsx({
[Classes.SKELETON]: loading,
})}
>
{amount}
</BankAccountBalanceAmount>
<BankAccountBalanceLabel>{intl.get('balance')}</BankAccountBalanceLabel>
</BankAccountBalanceWrap>
);
}
function BankAccountTypeIcon({ type }) {
const icon = ACCOUNT_TYPE_PAIR_ICON[type];
if (!icon) {
return;
}
return (
<AccountIconWrap>
<Icon icon={icon} iconSize={18} />
</AccountIconWrap>
);
}
export function BankAccount({
title,
code,
type,
balance,
loading = false,
updatedBeforeText,
...restProps
}) {
return (
<BankAccountWrap {...restProps}>
<BankAccountHeader>
<BankAccountTitle className={clsx({ [Classes.SKELETON]: loading })}>
{title}
</BankAccountTitle>
<BnakAccountCode className={clsx({ [Classes.SKELETON]: loading })}>
{code}
</BnakAccountCode>
{!loading && <BankAccountTypeIcon type={type} />}
</BankAccountHeader>
<BankAccountMeta>
<BankAccountMetaLine
title={intl.get('cash_flow.label_account_transcations')}
value={2}
className={clsx({ [Classes.SKELETON]: loading })}
/>
<BankAccountMetaLine
title={updatedBeforeText}
className={clsx({ [Classes.SKELETON]: loading })}
/>
</BankAccountMeta>
<BankAccountBalance amount={balance} loading={loading} />
</BankAccountWrap>
);
}
const BankAccountWrap = styled.div`
width: 225px;
height: 180px;
display: flex;
flex-direction: column;
border-radius: 3px;
background: #fff;
margin: 8px;
border: 1px solid #c8cad0;
transition: all 0.1s ease-in-out;
&:hover {
border-color: #0153cc;
}
`;
const BankAccountHeader = styled.div`
padding: 10px 12px;
padding-top: 16px;
position: relative;
`;
const BankAccountTitle = styled.div`
font-size: 15px;
font-style: inherit;
letter-spacing: -0.003em;
color: rgb(23, 43, 77);
white-space: nowrap;
font-weight: 600;
line-height: 1;
overflow: hidden;
text-overflow: ellipsis;
margin: 0px;
`;
const BnakAccountCode = styled.div`
font-size: 11px;
margin-top: 4px;
color: rgb(23, 43, 77);
display: inline-block;
`;
const BankAccountBalanceWrap = styled.div`
display: flex;
flex-direction: column;
margin-top: auto;
border-top: 1px solid #dfdfdf;
padding: 10px 12px;
`;
const BankAccountBalanceAmount = styled.div`
font-size: 16px;
font-weight: 600;
line-height: 1;
color: #57657e;
`;
const BankAccountBalanceLabel = styled.div`
text-transform: uppercase;
font-size: 10px;
letter-spacing: 0.5px;
margin-top: 3px;
opacity: 0.6;
`;
const MetaLineWrap = styled.div`
font-size: 11px;
display: flex;
color: #2f3c58;
&:not(:first-of-type) {
margin-top: 6px;
}
`;
const MetaLineTitle = styled.div``;
const MetaLineValue = styled.div`
box-sizing: border-box;
font-style: inherit;
background: rgb(223, 225, 230);
line-height: initial;
align-content: center;
padding: 0px 2px;
border-radius: 9.6px;
font-weight: normal;
text-transform: none;
width: 30px;
min-width: 30px;
height: 16px;
text-align: center;
color: rgb(23, 43, 77);
font-size: 11px;
margin-left: auto;
`;
const BankAccountMeta = styled.div`
padding: 0 12px 10px;
`;
export const BankAccountsList = styled.div`
display: flex;
margin: -8px;
flex-wrap: wrap;
`;
const AccountIconWrap = styled.div`
position: absolute;
top: 14px;
color: #abb3bb;
right: 12px;
`;

View File

@@ -0,0 +1,205 @@
import React from 'react';
import intl from 'react-intl-universal';
import styled from 'styled-components';
import clsx from 'classnames';
import { Classes } from '@blueprintjs/core';
import { Icon } from '@/components/Icon';
const ACCOUNT_TYPE = {
CASH: 'cash',
BANK: 'bank',
CREDIT_CARD: 'credit-card',
};
const ACCOUNT_TYPE_PAIR_ICON = {
[ACCOUNT_TYPE.CASH]: 'payments',
[ACCOUNT_TYPE.CREDIT_CARD]: 'credit-card',
[ACCOUNT_TYPE.BANK]: 'account-balance',
};
function BankAccountMetaLine({ title, value, className }) {
return (
<MetaLineWrap className={className}>
<MetaLineTitle>{title}</MetaLineTitle>
{value && <MetaLineValue>{value}</MetaLineValue>}
</MetaLineWrap>
);
}
function BankAccountBalance({ amount, loading }) {
return (
<BankAccountBalanceWrap>
<BankAccountBalanceAmount
className={clsx({
[Classes.SKELETON]: loading,
})}
>
{amount}
</BankAccountBalanceAmount>
<BankAccountBalanceLabel>{intl.get('balance')}</BankAccountBalanceLabel>
</BankAccountBalanceWrap>
);
}
function BankAccountTypeIcon({ type }) {
const icon = ACCOUNT_TYPE_PAIR_ICON[type];
if (!icon) {
return;
}
return (
<AccountIconWrap>
<Icon icon={icon} iconSize={18} />
</AccountIconWrap>
);
}
export function BankAccount({
title,
code,
type,
balance,
loading = false,
updatedBeforeText,
...restProps
}) {
return (
<BankAccountWrap {...restProps}>
<BankAccountHeader>
<BankAccountTitle className={clsx({ [Classes.SKELETON]: loading })}>
{title}
</BankAccountTitle>
<BnakAccountCode className={clsx({ [Classes.SKELETON]: loading })}>
{code}
</BnakAccountCode>
{!loading && <BankAccountTypeIcon type={type} />}
</BankAccountHeader>
<BankAccountMeta>
<BankAccountMetaLine
title={intl.get('cash_flow.label_account_transcations')}
value={2}
className={clsx({ [Classes.SKELETON]: loading })}
/>
<BankAccountMetaLine
title={updatedBeforeText}
className={clsx({ [Classes.SKELETON]: loading })}
/>
</BankAccountMeta>
<BankAccountBalance amount={balance} loading={loading} />
</BankAccountWrap>
);
}
const BankAccountWrap = styled.div`
width: 225px;
height: 180px;
display: flex;
flex-direction: column;
border-radius: 3px;
background: #fff;
margin: 8px;
border: 1px solid #c8cad0;
transition: all 0.1s ease-in-out;
&:hover {
border-color: #0153cc;
}
`;
const BankAccountHeader = styled.div`
padding: 10px 12px;
padding-top: 16px;
position: relative;
`;
const BankAccountTitle = styled.div`
font-size: 15px;
font-style: inherit;
letter-spacing: -0.003em;
color: rgb(23, 43, 77);
white-space: nowrap;
font-weight: 600;
line-height: 1;
overflow: hidden;
text-overflow: ellipsis;
margin: 0px;
`;
const BnakAccountCode = styled.div`
font-size: 11px;
margin-top: 4px;
color: rgb(23, 43, 77);
display: inline-block;
`;
const BankAccountBalanceWrap = styled.div`
display: flex;
flex-direction: column;
margin-top: auto;
border-top: 1px solid #dfdfdf;
padding: 10px 12px;
`;
const BankAccountBalanceAmount = styled.div`
font-size: 16px;
font-weight: 600;
line-height: 1;
color: #57657e;
`;
const BankAccountBalanceLabel = styled.div`
text-transform: uppercase;
font-size: 10px;
letter-spacing: 0.5px;
margin-top: 3px;
opacity: 0.6;
`;
const MetaLineWrap = styled.div`
font-size: 11px;
display: flex;
color: #2f3c58;
&:not(:first-of-type) {
margin-top: 6px;
}
`;
const MetaLineTitle = styled.div``;
const MetaLineValue = styled.div`
box-sizing: border-box;
font-style: inherit;
background: rgb(223, 225, 230);
line-height: initial;
align-content: center;
padding: 0px 2px;
border-radius: 9.6px;
font-weight: normal;
text-transform: none;
width: 30px;
min-width: 30px;
height: 16px;
text-align: center;
color: rgb(23, 43, 77);
font-size: 11px;
margin-left: auto;
`;
const BankAccountMeta = styled.div`
padding: 0 12px 10px;
`;
export const BankAccountsList = styled.div`
display: flex;
margin: -8px;
flex-wrap: wrap;
`;
const AccountIconWrap = styled.div`
position: absolute;
top: 14px;
color: #abb3bb;
right: 12px;
`;

View File

@@ -1,18 +0,0 @@
import React from 'react';
import * as R from 'ramda';
import styled from 'styled-components';
import { CurrencyTag } from 'components';
import withCurrentOrganization from 'containers/Organization/withCurrentOrganization';
/**
* base currecncy.
*/
function BaseCurrency({
// #withCurrentOrganization
organization: { base_currency },
}) {
return <CurrencyTag>{base_currency}</CurrencyTag>;
}
export default R.compose(withCurrentOrganization())(BaseCurrency);

View File

@@ -1,124 +0,0 @@
import React from 'react';
import intl from 'react-intl-universal';
import { MenuItem } from '@blueprintjs/core';
import { Suggest } from '@blueprintjs/select';
import { FormattedMessage as T } from 'components';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
/**
* branch suggest field.
* @returns
*/
export default function BranchSuggestField({
branches,
initialBranchId,
selectedBranchId,
defaultSelectText = intl.get('select_branch'),
popoverFill = false,
onBranchSelected,
...suggestProps
}) {
const initialBranch = React.useMemo(
() => branches.find((b) => b.id === initialBranchId),
[initialBranchId, branches],
);
const [selectedBranch, setSelectedBranch] = React.useState(
initialBranch || null,
);
React.useEffect(() => {
if (typeof selectedBranchId !== 'undefined') {
const branch = selectedBranchId
? branches.find((a) => a.id === selectedBranchId)
: null;
setSelectedBranch(branch);
}
}, [selectedBranchId, branches, setSelectedBranch]);
/**
*
* @param {*} branch
* @returns
*/
const branchItemRenderer = (branch, { handleClick, modifiers, query }) => {
return (
<MenuItem
// active={modifiers.active}
disabled={modifiers.disabled}
label={branch.code}
key={branch.id}
onClick={handleClick}
text={branch.name}
/>
);
};
/**
*
* @param {*} query
* @param {*} branch
* @param {*} _index
* @param {*} exactMatch
* @returns
*/
const branchItemPredicate = (query, branch, _index, exactMatch) => {
const normalizedTitle = branch.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${branch.code}. ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
}
};
/**
*
* @param {*} branch
* @returns
*/
const brnachItemSelect = React.useCallback(
(branch) => {
if (branch.id) {
setSelectedBranch({ ...branch });
onBranchSelected && onBranchSelected(branch);
}
},
[setSelectedBranch, onBranchSelected],
);
/**
*
* @param {*} inputVaue
* @returns
*/
const branchInputValueRenderer = (inputValue) => {
if (inputValue) {
return inputValue.name.toString();
}
return '';
};
return (
<Suggest
items={branches}
noResults={<MenuItem disabled={true} text={<T id={'no_accounts'} />} />}
itemRenderer={branchItemRenderer}
itemPredicate={branchItemPredicate}
onItemSelect={brnachItemSelect}
selectedItem={selectedBranch}
inputProps={{ placeholder: defaultSelectText }}
resetOnClose={true}
fill={true}
popoverProps={{ minimal: true, boundary: 'window' }}
inputValueRenderer={branchInputValueRenderer}
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
{...suggestProps}
/>
);
}

View File

@@ -0,0 +1,124 @@
import React from 'react';
import intl from 'react-intl-universal';
import { MenuItem } from '@blueprintjs/core';
import { Suggest } from '@blueprintjs/select';
import { FormattedMessage as T } from '@/components';
import classNames from 'classnames';
import { CLASSES } from '@/constants/classes';
/**
* branch suggest field.
* @returns
*/
export function BranchSuggestField({
branches,
initialBranchId,
selectedBranchId,
defaultSelectText = intl.get('select_branch'),
popoverFill = false,
onBranchSelected,
...suggestProps
}) {
const initialBranch = React.useMemo(
() => branches.find((b) => b.id === initialBranchId),
[initialBranchId, branches],
);
const [selectedBranch, setSelectedBranch] = React.useState(
initialBranch || null,
);
React.useEffect(() => {
if (typeof selectedBranchId !== 'undefined') {
const branch = selectedBranchId
? branches.find((a) => a.id === selectedBranchId)
: null;
setSelectedBranch(branch);
}
}, [selectedBranchId, branches, setSelectedBranch]);
/**
*
* @param {*} branch
* @returns
*/
const branchItemRenderer = (branch, { handleClick, modifiers, query }) => {
return (
<MenuItem
// active={modifiers.active}
disabled={modifiers.disabled}
label={branch.code}
key={branch.id}
onClick={handleClick}
text={branch.name}
/>
);
};
/**
*
* @param {*} query
* @param {*} branch
* @param {*} _index
* @param {*} exactMatch
* @returns
*/
const branchItemPredicate = (query, branch, _index, exactMatch) => {
const normalizedTitle = branch.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${branch.code}. ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
}
};
/**
*
* @param {*} branch
* @returns
*/
const brnachItemSelect = React.useCallback(
(branch) => {
if (branch.id) {
setSelectedBranch({ ...branch });
onBranchSelected && onBranchSelected(branch);
}
},
[setSelectedBranch, onBranchSelected],
);
/**
*
* @param {*} inputVaue
* @returns
*/
const branchInputValueRenderer = (inputValue) => {
if (inputValue) {
return inputValue.name.toString();
}
return '';
};
return (
<Suggest
items={branches}
noResults={<MenuItem disabled={true} text={<T id={'no_accounts'} />} />}
itemRenderer={branchItemRenderer}
itemPredicate={branchItemPredicate}
onItemSelect={brnachItemSelect}
selectedItem={selectedBranch}
inputProps={{ placeholder: defaultSelectText }}
resetOnClose={true}
fill={true}
popoverProps={{ minimal: true, boundary: 'window' }}
inputValueRenderer={branchInputValueRenderer}
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
{...suggestProps}
/>
);
}

View File

@@ -1,2 +0,0 @@
export * from './BranchSelect';
export * from './BranchMultiSelect'

View File

@@ -0,0 +1,3 @@
export * from './BranchSelect';
export * from './BranchMultiSelect';
export * from './BranchSuggestField';

View File

@@ -1,59 +0,0 @@
import React, { useCallback } from 'react';
import { FormattedMessage as T } from 'components';
import { ListSelect } from 'components';
import { MenuItem } from '@blueprintjs/core';
import { saveInvoke } from 'utils';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
export default function CategoriesSelectList({
categories,
selecetedCategoryId,
defaultSelectText = <T id={'select_category'} />,
onCategorySelected,
popoverFill = false,
className,
...restProps
}) {
// Filter Items Category
const filterItemCategory = (query, item, _index, exactMatch) => {
const normalizedTitle = item.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${item.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
}
};
const handleItemCategorySelected = useCallback(
(ItemCategory) => saveInvoke(onCategorySelected, ItemCategory),
[onCategorySelected],
);
const categoryItem = useCallback(
(item, { handleClick }) => (
<MenuItem key={item.id} text={item.name} onClick={handleClick} />
),
[],
);
return (
<ListSelect
items={categories}
selectedItemProp={'id'}
selectedItem={selecetedCategoryId}
textProp={'name'}
defaultText={defaultSelectText}
onItemSelect={handleItemCategorySelected}
itemPredicate={filterItemCategory}
itemRenderer={categoryItem}
popoverProps={{ minimal: true, usePortal: !popoverFill }}
className={classNames('form-group--select-list', {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
}, className)}
{...restProps}
/>
);
}

View File

@@ -1,23 +0,0 @@
import React from 'react';
import classNames from 'classnames';
import { Spinner } from '@blueprintjs/core';
import { CLASSES } from 'common/classes';
import If from './Utils/If';
export default function CloudLoadingIndicator({
isLoading,
children,
}) {
return (
<div className={classNames(
CLASSES.CLOUD_SPINNER,
{ [CLASSES.IS_LOADING]: isLoading },
)}>
<If condition={isLoading}>
<Spinner size={30} value={null} />
</If>
{ children }
</div>
);
}

View File

@@ -1,25 +0,0 @@
import styled from 'styled-components';
import { Card } from '../Card';
import DataTable from '../DataTable';
export const CommercialDocBox = styled(Card)`
padding: 22px 20px;
`;
export const CommercialDocHeader = styled.div`
margin-bottom: 25px;
`;
export const CommercialDocTopHeader = styled.div`
margin-bottom: 30px;
`;
export const CommercialDocEntriesTable = styled(DataTable)`
.tbody .tr:last-child .td {
border-bottom: 1px solid #d2dce2;
}
`;
export const CommercialDocFooter = styled.div`
margin-top: 28px;
`;

View File

@@ -0,0 +1,25 @@
import styled from 'styled-components';
import { Card } from '../Card';
import { DataTable } from '../Datatable';
export const CommercialDocBox = styled(Card)`
padding: 22px 20px;
`;
export const CommercialDocHeader = styled.div`
margin-bottom: 25px;
`;
export const CommercialDocTopHeader = styled.div`
margin-bottom: 30px;
`;
export const CommercialDocEntriesTable = styled(DataTable)`
.tbody .tr:last-child .td {
border-bottom: 1px solid #d2dce2;
}
`;
export const CommercialDocFooter = styled.div`
margin-top: 28px;
`;

View File

@@ -1,110 +0,0 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
export default function ContactSelecetList({
contactsList,
initialContactId,
selectedContactId,
createNewItemFrom,
defaultSelectText = <T id={'select_contact'} />,
onContactSelected,
popoverFill = false,
disabled = false,
buttonProps,
...restProps
}) {
const contacts = useMemo(
() =>
contactsList.map((contact) => ({
...contact,
_id: `${contact.id}_${contact.contact_type}`,
})),
[contactsList],
);
const initialContact = useMemo(
() => contacts.find((a) => a.id === initialContactId),
[initialContactId, contacts],
);
const [selecetedContact, setSelectedContact] = useState(
initialContact || null,
);
useEffect(() => {
if (typeof selectedContactId !== 'undefined') {
const account = selectedContactId
? contacts.find((a) => a.id === selectedContactId)
: null;
setSelectedContact(account);
}
}, [selectedContactId, contacts, setSelectedContact]);
const handleContactRenderer = useCallback(
(contact, { handleClick }) => (
<MenuItem
key={contact.id}
text={contact.display_name}
onClick={handleClick}
/>
),
[],
);
const onContactSelect = useCallback(
(contact) => {
setSelectedContact({ ...contact });
onContactSelected && onContactSelected(contact);
},
[setSelectedContact, onContactSelected],
);
// Filter Contact List
const itemPredicate = (query, contact, index, exactMatch) => {
const normalizedTitle = contact.display_name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return (
`${contact.display_name} ${normalizedTitle}`.indexOf(normalizedQuery) >=
0
);
}
};
return (
<Select
items={contacts}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
itemRenderer={handleContactRenderer}
itemPredicate={itemPredicate}
filterable={true}
disabled={disabled}
onItemSelect={onContactSelect}
popoverProps={{ minimal: true, usePortal: !popoverFill }}
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
inputProps={{
placeholder: intl.get('filter_'),
}}
{...restProps}
>
<Button
disabled={disabled}
text={
selecetedContact ? selecetedContact.display_name : defaultSelectText
}
{...buttonProps}
/>
</Select>
);
}

View File

@@ -0,0 +1,110 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { FormattedMessage as T } from '@/components';
import intl from 'react-intl-universal';
import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import classNames from 'classnames';
import { CLASSES } from '@/constants/classes';
export function ContactSelecetList({
contactsList,
initialContactId,
selectedContactId,
createNewItemFrom,
defaultSelectText = <T id={'select_contact'} />,
onContactSelected,
popoverFill = false,
disabled = false,
buttonProps,
...restProps
}) {
const contacts = useMemo(
() =>
contactsList.map((contact) => ({
...contact,
_id: `${contact.id}_${contact.contact_type}`,
})),
[contactsList],
);
const initialContact = useMemo(
() => contacts.find((a) => a.id === initialContactId),
[initialContactId, contacts],
);
const [selecetedContact, setSelectedContact] = useState(
initialContact || null,
);
useEffect(() => {
if (typeof selectedContactId !== 'undefined') {
const account = selectedContactId
? contacts.find((a) => a.id === selectedContactId)
: null;
setSelectedContact(account);
}
}, [selectedContactId, contacts, setSelectedContact]);
const handleContactRenderer = useCallback(
(contact, { handleClick }) => (
<MenuItem
key={contact.id}
text={contact.display_name}
onClick={handleClick}
/>
),
[],
);
const onContactSelect = useCallback(
(contact) => {
setSelectedContact({ ...contact });
onContactSelected && onContactSelected(contact);
},
[setSelectedContact, onContactSelected],
);
// Filter Contact List
const itemPredicate = (query, contact, index, exactMatch) => {
const normalizedTitle = contact.display_name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return (
`${contact.display_name} ${normalizedTitle}`.indexOf(normalizedQuery) >=
0
);
}
};
return (
<Select
items={contacts}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
itemRenderer={handleContactRenderer}
itemPredicate={itemPredicate}
filterable={true}
disabled={disabled}
onItemSelect={onContactSelect}
popoverProps={{ minimal: true, usePortal: !popoverFill }}
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
inputProps={{
placeholder: intl.get('filter_'),
}}
{...restProps}
>
<Button
disabled={disabled}
text={
selecetedContact ? selecetedContact.display_name : defaultSelectText
}
{...buttonProps}
/>
</Select>
);
}

View File

@@ -1,86 +0,0 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { itemPredicate, handleContactRenderer } from './utils';
export default function ContactSelectField({
contacts,
initialContactId,
selectedContactId,
defaultSelectText = <T id={'select_contact'} />,
onContactSelected,
popoverFill = false,
disabled = false,
buttonProps,
...restProps
}) {
const localContacts = useMemo(
() =>
contacts.map((contact) => ({
...contact,
_id: `${contact.id}_${contact.contact_type}`,
})),
[contacts],
);
const initialContact = useMemo(
() => contacts.find((a) => a.id === initialContactId),
[initialContactId, contacts],
);
const [selecetedContact, setSelectedContact] = useState(
initialContact || null,
);
useEffect(() => {
if (typeof selectedContactId !== 'undefined') {
const account = selectedContactId
? contacts.find((a) => a.id === selectedContactId)
: null;
setSelectedContact(account);
}
}, [selectedContactId, contacts, setSelectedContact]);
const handleContactSelect = useCallback(
(contact) => {
setSelectedContact({ ...contact });
onContactSelected && onContactSelected(contact);
},
[setSelectedContact, onContactSelected],
);
return (
<Select
items={localContacts}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
itemRenderer={handleContactRenderer}
itemPredicate={itemPredicate}
filterable={true}
disabled={disabled}
onItemSelect={handleContactSelect}
popoverProps={{ minimal: true, usePortal: !popoverFill }}
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
inputProps={{
placeholder: intl.get('filter_'),
}}
{...restProps}
>
<Button
disabled={disabled}
text={
selecetedContact ? selecetedContact.display_name : defaultSelectText
}
{...buttonProps}
/>
</Select>
);
}

View File

@@ -0,0 +1,86 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { FormattedMessage as T } from '@/components';
import intl from 'react-intl-universal';
import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import classNames from 'classnames';
import { CLASSES } from '@/constants/classes';
import { itemPredicate, handleContactRenderer } from './utils';
export function ContactSelectField({
contacts,
initialContactId,
selectedContactId,
defaultSelectText = <T id={'select_contact'} />,
onContactSelected,
popoverFill = false,
disabled = false,
buttonProps,
...restProps
}) {
const localContacts = useMemo(
() =>
contacts.map((contact) => ({
...contact,
_id: `${contact.id}_${contact.contact_type}`,
})),
[contacts],
);
const initialContact = useMemo(
() => contacts.find((a) => a.id === initialContactId),
[initialContactId, contacts],
);
const [selecetedContact, setSelectedContact] = useState(
initialContact || null,
);
useEffect(() => {
if (typeof selectedContactId !== 'undefined') {
const account = selectedContactId
? contacts.find((a) => a.id === selectedContactId)
: null;
setSelectedContact(account);
}
}, [selectedContactId, contacts, setSelectedContact]);
const handleContactSelect = useCallback(
(contact) => {
setSelectedContact({ ...contact });
onContactSelected && onContactSelected(contact);
},
[setSelectedContact, onContactSelected],
);
return (
<Select
items={localContacts}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
itemRenderer={handleContactRenderer}
itemPredicate={itemPredicate}
filterable={true}
disabled={disabled}
onItemSelect={handleContactSelect}
popoverProps={{ minimal: true, usePortal: !popoverFill }}
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
inputProps={{
placeholder: intl.get('filter_'),
}}
{...restProps}
>
<Button
disabled={disabled}
text={
selecetedContact ? selecetedContact.display_name : defaultSelectText
}
{...buttonProps}
/>
</Select>
);
}

View File

@@ -0,0 +1,42 @@
import React, { useCallback } from 'react';
import { MenuItem } from '@blueprintjs/core';
import { MultiSelect } from '../MultiSelectTaggable';
/**
* Contacts multi-select component.
*/
export function ContactsMultiSelect({ ...multiSelectProps }) {
// Filters accounts items.
const filterContactsPredicater = useCallback(
(query, contact, _index, exactMatch) => {
const normalizedTitle = contact.display_name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return normalizedTitle.indexOf(normalizedQuery) >= 0;
}
},
[],
);
return (
<MultiSelect
itemRenderer={(contact, { selected, active, handleClick }) => (
<MenuItem
active={active}
icon={selected ? 'tick' : 'blank'}
text={contact.display_name}
key={contact.id}
onClick={handleClick}
/>
)}
popoverProps={{ minimal: true }}
fill={true}
itemPredicate={filterContactsPredicater}
tagRenderer={(item) => item.display_name}
{...multiSelectProps}
/>
);
}

View File

@@ -0,0 +1,107 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { MenuItem } from '@blueprintjs/core';
import { Suggest } from '@blueprintjs/select';
import { FormattedMessage as T } from '@/components';
import classNames from 'classnames';
import { CLASSES } from '@/constants/classes';
import intl from 'react-intl-universal';
export function ContactsSuggestField({
contactsList,
initialContactId,
selectedContactId,
defaultTextSelect = intl.get('select_contact'),
onContactSelected,
selectedContactType = [],
popoverFill = false,
...suggestProps
}) {
// filteredContacts
const contacts = useMemo(
() =>
contactsList.map((contact) => ({
...contact,
_id: `${contact.id}_${contact.contact_type}`,
})),
[contactsList],
);
const initialContact = useMemo(
() => contacts.find((a) => a.id === initialContactId),
[initialContactId, contacts],
);
const [selecetedContact, setSelectedContact] = useState(
initialContact || null,
);
useEffect(() => {
if (typeof selectedContactId !== 'undefined') {
const contact = selectedContactId
? contacts.find((a) => a.id === selectedContactId)
: null;
setSelectedContact(contact);
}
}, [selectedContactId, contacts, setSelectedContact]);
const contactRenderer = useCallback(
(contact, { handleClick }) => (
<MenuItem
key={contact.id}
text={contact.display_name}
label={contact.formatted_contact_service}
onClick={handleClick}
/>
),
[],
);
const onContactSelect = useCallback(
(contact) => {
setSelectedContact({ ...contact });
onContactSelected && onContactSelected(contact);
},
[setSelectedContact, onContactSelected],
);
const handleInputValueRenderer = (inputValue) => {
if (inputValue) {
return inputValue.display_name.toString();
}
};
const filterContacts = (query, contact, index, exactMatch) => {
const normalizedTitle = contact.display_name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return (
`${contact.display_name} ${normalizedTitle}`.indexOf(normalizedQuery) >=
0
);
}
};
return (
<Suggest
items={contacts}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
itemRenderer={contactRenderer}
itemPredicate={filterContacts}
onItemSelect={onContactSelect}
selectedItem={selecetedContact}
inputProps={{ placeholder: defaultTextSelect }}
resetOnClose={true}
popoverProps={{ minimal: true, boundary: 'window' }}
inputValueRenderer={handleInputValueRenderer}
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
{...suggestProps}
/>
);
}

View File

@@ -1,116 +0,0 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import * as R from 'ramda';
import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import {
itemPredicate,
handleContactRenderer,
createNewItemRenderer,
createNewItemFromQuery,
} from './utils';
import withDrawerActions from 'containers/Drawer/withDrawerActions';
import { DRAWERS } from 'common/drawers';
function CustomerSelectField({
// #withDrawerActions
openDrawer,
// #ownProps
contacts,
initialContactId,
selectedContactId,
defaultSelectText = <T id={'select_contact'} />,
onContactSelected,
popoverFill = false,
disabled = false,
allowCreate,
buttonProps,
...restProps
}) {
const localContacts = useMemo(
() =>
contacts.map((contact) => ({
...contact,
_id: `${contact.id}_${contact.contact_type}`,
})),
[contacts],
);
const initialContact = useMemo(
() => contacts.find((a) => a.id === initialContactId),
[initialContactId, contacts],
);
const [selecetedContact, setSelectedContact] = useState(
initialContact || null,
);
useEffect(() => {
if (typeof selectedContactId !== 'undefined') {
const account = selectedContactId
? contacts.find((a) => a.id === selectedContactId)
: null;
setSelectedContact(account);
}
}, [selectedContactId, contacts, setSelectedContact]);
const handleContactSelect = useCallback(
(contact) => {
if (contact.id) {
setSelectedContact({ ...contact });
onContactSelected && onContactSelected(contact);
} else {
openDrawer(DRAWERS.QUICK_CREATE_CUSTOMER);
}
},
[setSelectedContact, onContactSelected, openDrawer],
);
// Maybe inject create new item props to suggest component.
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
const maybeCreateNewItemFromQuery = allowCreate
? createNewItemFromQuery
: null;
return (
<Select
items={localContacts}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
itemRenderer={handleContactRenderer}
itemPredicate={itemPredicate}
filterable={true}
disabled={disabled}
onItemSelect={handleContactSelect}
popoverProps={{ minimal: true, usePortal: !popoverFill }}
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
inputProps={{
placeholder: intl.get('filter_'),
}}
createNewItemRenderer={maybeCreateNewItemRenderer}
createNewItemFromQuery={maybeCreateNewItemFromQuery}
createNewItemPosition={'top'}
{...restProps}
>
<Button
disabled={disabled}
text={
selecetedContact ? selecetedContact.display_name : defaultSelectText
}
{...buttonProps}
/>
</Select>
);
}
export default R.compose(withDrawerActions)(CustomerSelectField);

View File

@@ -1,115 +0,0 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import * as R from 'ramda';
import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import {
itemPredicate,
handleContactRenderer,
createNewItemFromQuery,
createNewItemRenderer,
} from './utils';
import withDrawerActions from 'containers/Drawer/withDrawerActions';
import { DRAWERS } from 'common/drawers';
function VendorSelectField({
// #withDrawerActions
openDrawer,
// #ownProps
contacts,
initialContactId,
selectedContactId,
defaultSelectText = <T id={'select_contact'} />,
onContactSelected,
popoverFill = false,
disabled = false,
allowCreate,
buttonProps,
...restProps
}) {
const localContacts = useMemo(
() =>
contacts.map((contact) => ({
...contact,
_id: `${contact.id}_${contact.contact_type}`,
})),
[contacts],
);
const initialContact = useMemo(
() => contacts.find((a) => a.id === initialContactId),
[initialContactId, contacts],
);
const [selecetedContact, setSelectedContact] = useState(
initialContact || null,
);
useEffect(() => {
if (typeof selectedContactId !== 'undefined') {
const account = selectedContactId
? contacts.find((a) => a.id === selectedContactId)
: null;
setSelectedContact(account);
}
}, [selectedContactId, contacts, setSelectedContact]);
const handleContactSelect = useCallback(
(contact) => {
if (contact.id) {
setSelectedContact({ ...contact });
onContactSelected && onContactSelected(contact);
} else {
openDrawer(DRAWERS.QUICK_WRITE_VENDOR);
}
},
[setSelectedContact, onContactSelected, openDrawer],
);
// Maybe inject create new item props to suggest component.
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
const maybeCreateNewItemFromQuery = allowCreate
? createNewItemFromQuery
: null;
return (
<Select
items={localContacts}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
itemRenderer={handleContactRenderer}
itemPredicate={itemPredicate}
filterable={true}
disabled={disabled}
onItemSelect={handleContactSelect}
popoverProps={{ minimal: true, usePortal: !popoverFill }}
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
inputProps={{
placeholder: intl.get('filter_'),
}}
createNewItemRenderer={maybeCreateNewItemRenderer}
createNewItemFromQuery={maybeCreateNewItemFromQuery}
createNewItemPosition={'top'}
{...restProps}
>
<Button
disabled={disabled}
text={
selecetedContact ? selecetedContact.display_name : defaultSelectText
}
{...buttonProps}
/>
</Select>
);
}
export default R.compose(withDrawerActions)(VendorSelectField);

View File

@@ -1,5 +0,0 @@
import ContactSelectField from './ContactSelectField';
import CustomerSelectField from './CustomerSelectField';
import VendorSelectField from './VendorSelectField';
export { ContactSelectField, CustomerSelectField, VendorSelectField };

View File

@@ -0,0 +1,4 @@
export * from './ContactSelectField';
export * from './ContactsSuggestField';
export * from './ContactSelecetList';
export * from './ContactsMultiSelect';

View File

@@ -1,42 +0,0 @@
import React, { useCallback } from 'react';
import { MenuItem } from '@blueprintjs/core';
import { MultiSelect } from '../components/MultiSelectTaggable';
/**
* Contacts multi-select component.
*/
export default function ContactsMultiSelect({ ...multiSelectProps }) {
// Filters accounts items.
const filterContactsPredicater = useCallback(
(query, contact, _index, exactMatch) => {
const normalizedTitle = contact.display_name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return normalizedTitle.indexOf(normalizedQuery) >= 0;
}
},
[],
);
return (
<MultiSelect
itemRenderer={(contact, { selected, active, handleClick }) => (
<MenuItem
active={active}
icon={selected ? 'tick' : 'blank'}
text={contact.display_name}
key={contact.id}
onClick={handleClick}
/>
)}
popoverProps={{ minimal: true }}
fill={true}
itemPredicate={filterContactsPredicater}
tagRenderer={(item) => item.display_name}
{...multiSelectProps}
/>
);
}

View File

@@ -1,107 +0,0 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { MenuItem } from '@blueprintjs/core';
import { Suggest } from '@blueprintjs/select';
import { FormattedMessage as T } from 'components';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import intl from 'react-intl-universal';
export default function ContactsSuggestField({
contactsList,
initialContactId,
selectedContactId,
defaultTextSelect = intl.get('select_contact'),
onContactSelected,
selectedContactType = [],
popoverFill = false,
...suggestProps
}) {
// filteredContacts
const contacts = useMemo(
() =>
contactsList.map((contact) => ({
...contact,
_id: `${contact.id}_${contact.contact_type}`,
})),
[contactsList],
);
const initialContact = useMemo(
() => contacts.find((a) => a.id === initialContactId),
[initialContactId, contacts],
);
const [selecetedContact, setSelectedContact] = useState(
initialContact || null,
);
useEffect(() => {
if (typeof selectedContactId !== 'undefined') {
const contact = selectedContactId
? contacts.find((a) => a.id === selectedContactId)
: null;
setSelectedContact(contact);
}
}, [selectedContactId, contacts, setSelectedContact]);
const contactRenderer = useCallback(
(contact, { handleClick }) => (
<MenuItem
key={contact.id}
text={contact.display_name}
label={contact.formatted_contact_service}
onClick={handleClick}
/>
),
[],
);
const onContactSelect = useCallback(
(contact) => {
setSelectedContact({ ...contact });
onContactSelected && onContactSelected(contact);
},
[setSelectedContact, onContactSelected],
);
const handleInputValueRenderer = (inputValue) => {
if (inputValue) {
return inputValue.display_name.toString();
}
};
const filterContacts = (query, contact, index, exactMatch) => {
const normalizedTitle = contact.display_name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return (
`${contact.display_name} ${normalizedTitle}`.indexOf(normalizedQuery) >=
0
);
}
};
return (
<Suggest
items={contacts}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
itemRenderer={contactRenderer}
itemPredicate={filterContacts}
onItemSelect={onContactSelect}
selectedItem={selecetedContact}
inputProps={{ placeholder: defaultTextSelect }}
resetOnClose={true}
popoverProps={{ minimal: true, boundary: 'window' }}
inputValueRenderer={handleInputValueRenderer}
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
{...suggestProps}
/>
);
}

View File

@@ -1,52 +0,0 @@
import React, { memo } from 'react';
import { Popover, Position, Classes } from '@blueprintjs/core';
import { saveInvoke } from 'utils';
const POPPER_MODIFIERS = {
preventOverflow: { boundariesElement: 'viewport' },
};
function ContextMenu(props) {
const { bindMenu, isOpen, children, onClosed, popoverProps } = props;
const handleClosed = () => {
requestAnimationFrame(() => saveInvoke(onClosed));
};
const handleInteraction = (nextOpenState) => {
if (!nextOpenState) {
// Delay the actual hiding till the event queue clears
// to avoid flicker of opening twice
requestAnimationFrame(() => saveInvoke(onClosed));
}
};
return (
<div className={Classes.CONTEXT_MENU_POPOVER_TARGET} {...bindMenu}>
<Popover
onClosed={handleClosed}
modifiers={POPPER_MODIFIERS}
content={children}
enforceFocus={true}
isOpen={isOpen}
minimal={true}
position={Position.RIGHT_TOP}
target={<div />}
usePortal={false}
onInteraction={handleInteraction}
{...popoverProps}
/>
</div>
);
}
export default memo(ContextMenu, (prevProps, nextProps) => {
if (
prevProps.isOpen === nextProps.isOpen &&
prevProps.bindMenu.style === nextProps.bindMenu.style
) {
return true;
} else {
return false;
}
});

View File

@@ -0,0 +1,52 @@
import React, { memo } from 'react';
import { Popover, Position, Classes } from '@blueprintjs/core';
import { saveInvoke } from '@/utils';
const POPPER_MODIFIERS = {
preventOverflow: { boundariesElement: 'viewport' },
};
function ContextMenu(props) {
const { bindMenu, isOpen, children, onClosed, popoverProps } = props;
const handleClosed = () => {
requestAnimationFrame(() => saveInvoke(onClosed));
};
const handleInteraction = (nextOpenState) => {
if (!nextOpenState) {
// Delay the actual hiding till the event queue clears
// to avoid flicker of opening twice
requestAnimationFrame(() => saveInvoke(onClosed));
}
};
return (
<div className={Classes.CONTEXT_MENU_POPOVER_TARGET} {...bindMenu}>
<Popover
onClosed={handleClosed}
modifiers={POPPER_MODIFIERS}
content={children}
enforceFocus={true}
isOpen={isOpen}
minimal={true}
position={Position.RIGHT_TOP}
target={<div />}
usePortal={false}
onInteraction={handleInteraction}
{...popoverProps}
/>
</div>
);
}
export default memo(ContextMenu, (prevProps, nextProps) => {
if (
prevProps.isOpen === nextProps.isOpen &&
prevProps.bindMenu.style === nextProps.bindMenu.style
) {
return true;
} else {
return false;
}
});

View File

@@ -1,10 +0,0 @@
import React from 'react';
import { CurrencyTag } from 'components';
/**
* base currecncy.
* @returns
*/
export function BaseCurrency({ currency }) {
return <CurrencyTag>{currency}</CurrencyTag>;
}

View File

@@ -0,0 +1,10 @@
import React from 'react';
import { CurrencyTag } from '@/components';
/**
* base currecncy.
* @returns
*/
export function BaseCurrency({ currency }) {
return <CurrencyTag>{currency}</CurrencyTag>;
}

View File

@@ -0,0 +1,58 @@
import React, { useCallback } from 'react';
import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
export function CurrenciesSelectList({ selectProps, onItemSelect, className }) {
const currencies = [
{
name: 'USD US dollars',
key: 'USD',
name: 'CAD Canadian dollars',
key: 'CAD',
},
];
// Handle currency item select.
const onCurrencySelect = useCallback(
(currency) => {
onItemSelect && onItemSelect(currency);
},
[onItemSelect],
);
// Filters currencies list.
const filterCurrenciesPredicator = useCallback(
(query, currency, _index, exactMatch) => {
const normalizedTitle = currency.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
return `${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
},
[],
);
// Currency item of select currencies field.
const currencyItem = (item, { handleClick, modifiers, query }) => {
return (
<MenuItem
text={item.name}
label={item.code}
key={item.id}
onClick={handleClick}
/>
);
};
return (
<Select
items={currencies}
noResults={<MenuItem disabled={true} text="No results." />}
itemRenderer={currencyItem}
itemPredicate={filterCurrenciesPredicator}
popoverProps={{ minimal: true }}
onItemSelect={onCurrencySelect}
{...selectProps}
>
<Button text={'USD US dollars'} />
</Select>
);
}

View File

@@ -0,0 +1,82 @@
import React, { useCallback, useEffect, useState } from 'react';
import { FormattedMessage as T } from '@/components';
import { CLASSES } from '@/constants/classes';
import classNames from 'classnames';
import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
export function CurrencySelectList({
currenciesList,
selectedCurrencyCode,
defaultSelectText = <T id={'select_currency_code'} />,
onCurrencySelected,
popoverFill = false,
disabled = false,
}) {
const [selectedCurrency, setSelectedCurrency] = useState(null);
// Filters currencies list.
const filterCurrencies = (query, currency, _index, exactMatch) => {
const normalizedTitle = currency.currency_code.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return (
`${currency.currency_code} ${normalizedTitle}`.indexOf(
normalizedQuery,
) >= 0
);
}
};
const onCurrencySelect = useCallback((currency) => {
setSelectedCurrency({ ...currency });
onCurrencySelected && onCurrencySelected(currency);
});
const currencyCodeRenderer = useCallback((CurrencyCode, { handleClick }) => {
return (
<MenuItem
key={CurrencyCode.id}
text={CurrencyCode.currency_code}
onClick={handleClick}
/>
);
}, []);
useEffect(() => {
if (typeof selectedCurrencyCode !== 'undefined') {
const currency = selectedCurrencyCode
? currenciesList.find((a) => a.currency_code === selectedCurrencyCode)
: null;
setSelectedCurrency(currency);
}
}, [selectedCurrencyCode, currenciesList, setSelectedCurrency]);
return (
<Select
items={currenciesList}
itemRenderer={currencyCodeRenderer}
itemPredicate={filterCurrencies}
onItemSelect={onCurrencySelect}
filterable={true}
popoverProps={{
minimal: true,
usePortal: !popoverFill,
inline: popoverFill,
}}
className={classNames('form-group--select-list', {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
>
<Button
disabled={disabled}
text={
selectedCurrency ? selectedCurrency.currency_code : defaultSelectText
}
/>
</Select>
);
}

View File

@@ -1,2 +0,0 @@
export * from './CurrencySelect';
export * from './BaseCurrency';

View File

@@ -0,0 +1,4 @@
export * from './CurrencySelect';
export * from './BaseCurrency';
export * from './CurrenciesSelectList';
export * from './CurrencySelectList';

View File

@@ -1,56 +0,0 @@
import React, {useCallback} from 'react';
import {
MenuItem,
Button,
} from '@blueprintjs/core';
import {
Select
} from '@blueprintjs/select';
export default function CurrenciesSelectList({
selectProps,
onItemSelect,
className,
}) {
const currencies = [{
name: 'USD US dollars', key: 'USD',
name: 'CAD Canadian dollars', key: 'CAD',
}];
// Handle currency item select.
const onCurrencySelect = useCallback((currency) => {
onItemSelect && onItemSelect(currency);
}, [onItemSelect]);
// Filters currencies list.
const filterCurrenciesPredicator = useCallback((query, currency, _index, exactMatch) => {
const normalizedTitle = currency.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
return `${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
}, []);
// Currency item of select currencies field.
const currencyItem = (item, { handleClick, modifiers, query }) => {
return (
<MenuItem text={item.name} label={item.code} key={item.id} onClick={handleClick} />
);
};
return (
<Select
items={currencies}
noResults={<MenuItem disabled={true} text='No results.' />}
itemRenderer={currencyItem}
itemPredicate={filterCurrenciesPredicator}
popoverProps={{ minimal: true }}
onItemSelect={onCurrencySelect}
{...selectProps}
>
<Button
text={'USD US dollars'}
/>
</Select>
);
}

View File

@@ -1,82 +0,0 @@
import React, { useCallback, useEffect, useState } from 'react';
import { FormattedMessage as T } from 'components';
import { CLASSES } from 'common/classes';
import classNames from 'classnames';
import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
export default function CurrencySelectList({
currenciesList,
selectedCurrencyCode,
defaultSelectText = <T id={'select_currency_code'} />,
onCurrencySelected,
popoverFill = false,
disabled = false,
}) {
const [selectedCurrency, setSelectedCurrency] = useState(null);
// Filters currencies list.
const filterCurrencies = (query, currency, _index, exactMatch) => {
const normalizedTitle = currency.currency_code.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return (
`${currency.currency_code} ${normalizedTitle}`.indexOf(
normalizedQuery,
) >= 0
);
}
};
const onCurrencySelect = useCallback((currency) => {
setSelectedCurrency({ ...currency });
onCurrencySelected && onCurrencySelected(currency);
});
const currencyCodeRenderer = useCallback((CurrencyCode, { handleClick }) => {
return (
<MenuItem
key={CurrencyCode.id}
text={CurrencyCode.currency_code}
onClick={handleClick}
/>
);
}, []);
useEffect(() => {
if (typeof selectedCurrencyCode !== 'undefined') {
const currency = selectedCurrencyCode
? currenciesList.find((a) => a.currency_code === selectedCurrencyCode)
: null;
setSelectedCurrency(currency);
}
}, [selectedCurrencyCode, currenciesList, setSelectedCurrency]);
return (
<Select
items={currenciesList}
itemRenderer={currencyCodeRenderer}
itemPredicate={filterCurrencies}
onItemSelect={onCurrencySelect}
filterable={true}
popoverProps={{
minimal: true,
usePortal: !popoverFill,
inline: popoverFill,
}}
className={classNames('form-group--select-list', {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
>
<Button
disabled={disabled}
text={
selectedCurrency ? selectedCurrency.currency_code : defaultSelectText
}
/>
</Select>
);
}

View File

@@ -1,109 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import {
Button,
MenuItem,
PopoverInteractionKind,
Position,
Classes,
} from '@blueprintjs/core';
import clsx from 'classnames';
import { defaultTo } from 'lodash';
import { Select } from '@blueprintjs/select';
import { FormattedMessage as T, Icon } from 'components';
/**
* warehouse & branches select list.
* @returns
*/
export default function CustomSelectList({
// #ownProps
items,
initialItemId,
selectedItemId,
loading = false,
defaultSelectText,
onItemSelected,
buttonProps,
}) {
const initialItem = React.useMemo(
() => items.find((a) => a.id === initialItemId),
[initialItemId, items],
);
const [selecetedItem, setSelectedItem] = React.useState(initialItem || null);
React.useEffect(() => {
if (typeof selectedItemId !== 'undefined') {
const item = selectedItemId
? items.find((a) => a.id === selectedItemId)
: null;
setSelectedItem(item);
}
}, [selectedItemId, items, setSelectedItem]);
// Menu items renderer.
const itemRenderer = (item, { handleClick, modifiers, query }) => (
<MenuItem
text={item.name}
key={item.id}
label={item.code}
onClick={handleClick}
/>
);
// Filters items items.
const filterItemsPredicater = (query, item, _index, exactMatch) => {
const normalizedTitle = item.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${item.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
}
};
const handleItemMenuSelect = React.useCallback(
(item) => {
if (item.id) {
setSelectedItem({ ...item });
onItemSelected && onItemSelected(item);
}
},
[onItemSelected, setSelectedItem],
);
return (
<Select
items={items}
noResults={<MenuItem disabled={true} text={<T id={'no_accounts'} />} />}
itemRenderer={itemRenderer}
itemPredicate={filterItemsPredicater}
onItemSelect={handleItemMenuSelect}
popoverProps={{
minimal: true,
position: Position.BOTTOM_LEFT,
interactionKind: PopoverInteractionKind.CLICK,
modifiers: {
offset: { offset: '0, 4' },
},
}}
className={clsx({ [Classes.SKELETON]: loading })}
>
<Button
text={
selecetedItem
? `${defaultSelectText}:${selecetedItem.name} ${defaultTo(
selecetedItem.code,
'',
)}`
: `${defaultSelectText}: Bigcapital`
}
minimal={true}
small={true}
{...buttonProps}
/>
</Select>
);
}

Some files were not shown because too many files have changed in this diff Show More