chrone: sperate client and server to different repos.

This commit is contained in:
a.bouhuolia
2021-09-21 17:13:53 +02:00
parent e011b2a82b
commit 18df5530c7
10015 changed files with 17686 additions and 97524 deletions

223
src/common/accountTypes.js Normal file
View File

@@ -0,0 +1,223 @@
export const ACCOUNT_TYPE = {
CASH: 'cash',
BANK: 'bank',
ACCOUNTS_RECEIVABLE: 'accounts-receivable',
INVENTORY: 'inventory',
OTHER_CURRENT_ASSET: 'other-ACCOUNT_PARENT_TYPE.CURRENT_ASSET',
FIXED_ASSET: 'fixed-asset',
NON_CURRENT_ASSET: 'non-ACCOUNT_PARENT_TYPE.CURRENT_ASSET',
ACCOUNTS_PAYABLE: 'accounts-payable',
CREDIT_CARD: 'credit-card',
TAX_PAYABLE: 'tax-payable',
OTHER_CURRENT_LIABILITY: 'other-current-liability',
LOGN_TERM_LIABILITY: 'long-term-liability',
NON_CURRENT_LIABILITY: 'non-current-liability',
EQUITY: 'equity',
INCOME: 'income',
OTHER_INCOME: 'other-income',
COST_OF_GOODS_SOLD: 'cost-of-goods-sold',
EXPENSE: 'expense',
OTHER_EXPENSE: 'other-expense',
};
export const ACCOUNT_PARENT_TYPE = {
CURRENT_ASSET: 'current-asset',
FIXED_ASSET: 'fixed-asset',
NON_CURRENT_ASSET: 'non-ACCOUNT_PARENT_TYPE.CURRENT_ASSET',
CURRENT_LIABILITY: 'current-liability',
LOGN_TERM_LIABILITY: 'long-term-liability',
NON_CURRENT_LIABILITY: 'non-current-liability',
EQUITY: 'equity',
EXPENSE: 'expense',
INCOME: 'income',
};
export const ACCOUNT_ROOT_TYPE = {
ASSET: 'asset',
LIABILITY: 'liability',
EQUITY: 'equity',
EXPENSE: 'expene',
INCOME: 'income',
};
export const ACCOUNT_NORMAL = {
CREDIT: 'credit',
DEBIT: 'debit',
};
export const ACCOUNT_TYPES = [
{
label: 'Cash',
key: ACCOUNT_TYPE.CASH,
normal: ACCOUNT_NORMAL.DEBIT,
parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET,
rootType: ACCOUNT_ROOT_TYPE.ASSET,
balanceSheet: true,
incomeSheet: false,
},
{
label: 'Bank',
key: ACCOUNT_TYPE.BANK,
normal: ACCOUNT_NORMAL.DEBIT,
parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET,
rootType: ACCOUNT_ROOT_TYPE.ASSET,
balanceSheet: true,
incomeSheet: false,
},
{
label: 'Accounts Receivable',
key: ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE,
normal: ACCOUNT_NORMAL.DEBIT,
rootType: ACCOUNT_ROOT_TYPE.ASSET,
parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET,
balanceSheet: true,
incomeSheet: false,
},
{
label: 'Inventory',
key: ACCOUNT_TYPE.INVENTORY,
normal: ACCOUNT_NORMAL.DEBIT,
rootType: ACCOUNT_ROOT_TYPE.ASSET,
parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET,
balanceSheet: true,
incomeSheet: false,
},
{
label: 'Other Current Asset',
key: ACCOUNT_TYPE.OTHER_CURRENT_ASSET,
normal: ACCOUNT_NORMAL.DEBIT,
rootType: ACCOUNT_ROOT_TYPE.ASSET,
parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET,
balanceSheet: true,
incomeSheet: false,
},
{
label: 'Fixed Asset',
key: ACCOUNT_TYPE.FIXED_ASSET,
normal: ACCOUNT_NORMAL.DEBIT,
rootType: ACCOUNT_ROOT_TYPE.ASSET,
parentType: ACCOUNT_PARENT_TYPE.FIXED_ASSET,
balanceSheet: true,
incomeSheet: false,
},
{
label: 'Non-Current Asset',
key: ACCOUNT_TYPE.NON_CURRENT_ASSET,
normal: ACCOUNT_NORMAL.DEBIT,
rootType: ACCOUNT_ROOT_TYPE.ASSET,
parentType: ACCOUNT_PARENT_TYPE.FIXED_ASSET,
balanceSheet: true,
incomeSheet: false,
},
{
label: 'Accounts Payable',
key: ACCOUNT_TYPE.ACCOUNTS_PAYABLE,
normal: ACCOUNT_NORMAL.CREDIT,
rootType: ACCOUNT_ROOT_TYPE.LIABILITY,
parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY,
balanceSheet: true,
incomeSheet: false,
},
{
label: 'Credit Card',
key: ACCOUNT_TYPE.CREDIT_CARD,
normal: ACCOUNT_NORMAL.CREDIT,
rootType: ACCOUNT_ROOT_TYPE.LIABILITY,
parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY,
balanceSheet: true,
incomeSheet: false,
},
{
label: 'Tax Payable',
key: ACCOUNT_TYPE.TAX_PAYABLE,
normal: ACCOUNT_NORMAL.CREDIT,
rootType: ACCOUNT_ROOT_TYPE.LIABILITY,
parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY,
balanceSheet: true,
incomeSheet: false,
},
{
label: 'Other Current Liability',
key: ACCOUNT_TYPE.OTHER_CURRENT_LIABILITY,
normal: ACCOUNT_NORMAL.CREDIT,
rootType: ACCOUNT_ROOT_TYPE.LIABILITY,
parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY,
balanceSheet: false,
incomeSheet: true,
},
{
label: 'Long Term Liability',
key: ACCOUNT_TYPE.LOGN_TERM_LIABILITY,
normal: ACCOUNT_NORMAL.CREDIT,
rootType: ACCOUNT_ROOT_TYPE.LIABILITY,
parentType: ACCOUNT_PARENT_TYPE.LOGN_TERM_LIABILITY,
balanceSheet: false,
incomeSheet: true,
},
{
label: 'Non-Current Liability',
key: ACCOUNT_TYPE.NON_CURRENT_LIABILITY,
normal: ACCOUNT_NORMAL.CREDIT,
rootType: ACCOUNT_ROOT_TYPE.LIABILITY,
parentType: ACCOUNT_PARENT_TYPE.NON_CURRENT_LIABILITY,
balanceSheet: false,
incomeSheet: true,
},
{
label: 'Equity',
key: ACCOUNT_TYPE.EQUITY,
normal: ACCOUNT_NORMAL.CREDIT,
rootType: ACCOUNT_ROOT_TYPE.EQUITY,
parentType: ACCOUNT_PARENT_TYPE.EQUITY,
balanceSheet: true,
incomeSheet: false,
},
{
label: 'Income',
key: ACCOUNT_TYPE.INCOME,
normal: ACCOUNT_NORMAL.CREDIT,
rootType: ACCOUNT_ROOT_TYPE.INCOME,
parentType: ACCOUNT_PARENT_TYPE.INCOME,
balanceSheet: false,
incomeSheet: true,
},
{
label: 'Other Income',
key: ACCOUNT_TYPE.OTHER_INCOME,
normal: ACCOUNT_NORMAL.CREDIT,
rootType: ACCOUNT_ROOT_TYPE.INCOME,
parentType: ACCOUNT_PARENT_TYPE.INCOME,
balanceSheet: false,
incomeSheet: true,
},
{
label: 'Cost of Goods Sold',
key: ACCOUNT_TYPE.COST_OF_GOODS_SOLD,
normal: ACCOUNT_NORMAL.DEBIT,
rootType: ACCOUNT_ROOT_TYPE.EXPENSE,
parentType: ACCOUNT_PARENT_TYPE.EXPENSE,
balanceSheet: false,
incomeSheet: true,
},
{
label: 'Expense',
key: ACCOUNT_TYPE.EXPENSE,
normal: ACCOUNT_NORMAL.DEBIT,
rootType: ACCOUNT_ROOT_TYPE.EXPENSE,
parentType: ACCOUNT_PARENT_TYPE.EXPENSE,
balanceSheet: false,
incomeSheet: true,
},
{
label: 'Other Expense',
key: ACCOUNT_TYPE.OTHER_EXPENSE,
normal: ACCOUNT_NORMAL.DEBIT,
rootType: ACCOUNT_ROOT_TYPE.EXPENSE,
parentType: ACCOUNT_PARENT_TYPE.EXPENSE,
balanceSheet: false,
incomeSheet: true,
},
];

View File

@@ -0,0 +1,6 @@
import intl from 'react-intl-universal';
export default [
{ name: intl.get('decrement'), value: 'decrement' },
{ name: intl.get('increment'), value: 'increment' },
]

View File

@@ -0,0 +1,6 @@
import intl from 'react-intl-universal';
export default [
{ name: intl.get('bills'), value: 'Bill' },
{ name: intl.get('expenses'), value: 'Expense' },
];

97
src/common/classes.js Normal file
View File

@@ -0,0 +1,97 @@
import { Classes } from '@blueprintjs/core';
const CLASSES = {
DASHBOARD_PAGE: 'dashboard__page',
DASHBOARD_DATATABLE: 'dashboard__datatable',
DASHBOARD_CARD: 'dashboard__card',
DASHBOARD_CARD_PAGE: 'dashboard__card--page',
DATATABLE_EDITOR: 'datatable-editor',
DATATABLE_EDITOR_ACTIONS: 'datatable-editor__actions',
DATATABLE_EDITOR_ITEMS_ENTRIES: 'items-entries-table',
DATATABLE_EDITOR_HAS_TOTAL_ROW: 'has-total-row',
DASHBOARD_CONTENT: 'dashboard-content',
DASHBOARD_CONTENT_PREFERENCES: 'dashboard-content--preferences',
DASHBOARD_CONTENT_PANE: 'Pane2',
DASHBOARD_CENTERED_EMPTY_STATUS: 'dashboard__centered-empty-status',
PAGE_FORM: 'page-form',
PAGE_FORM_HEADER: 'page-form__header',
PAGE_FORM_HEADER_PRIMARY: 'page-form__primary-section',
PAGE_FORM_HEADER_FIELDS: 'page-form__header-fields',
PAGE_FORM_HEADER_BIG_NUMBERS: 'page-form__big-numbers',
PAGE_FORM_TABS: 'page-form__tabs',
PAGE_FORM_BODY: 'page-form__body',
PAGE_FORM_STRIP_STYLE: 'page-form--strip',
PAGE_FORM_FOOTER: 'page-form__footer',
PAGE_FORM_FLOATING_ACTIONS: 'page-form__floating-actions',
PAGE_FORM_BILL: 'page-form--bill',
PAGE_FORM_ESTIMATE: 'page-form--estimate',
PAGE_FORM_INVOICE: 'page-form--invoice',
PAGE_FORM_RECEIPT: 'page-form--receipt',
PAGE_FORM_PAYMENT_MADE: 'page-form--payment-made',
PAGE_FORM_PAYMENT_RECEIVE: 'page-form--payment-receive',
PAGE_FORM_CUSTOMER: 'page-form--customer',
PAGE_FORM_VENDOR: 'page-form--vendor',
PAGE_FORM_ITEM: 'page-form--item',
PAGE_FORM_MAKE_JOURNAL: 'page-form--make-journal-entries',
PAGE_FORM_EXPENSE: 'page-form--expense',
FORM_GROUP_LIST_SELECT: 'form-group--select-list',
CLOUD_SPINNER: 'cloud-spinner',
IS_LOADING: 'is-loading',
DATATABLE_EMPTY_STATUS: 'datatable-empty-status',
DATATABLE_EMPTY_STATUS_TITLE: 'datatable-empty-status__title',
DATATABLE_EMPTY_STATUS_DESC: 'datatable-empty-status__desc',
DATATABLE_EMPTY_STATUS_ACTIONS: 'datatable-empty-status__actions',
SELECT_LIST_FILL_POPOVER: 'select-list--fill-popover',
PREFERENCES_PAGE: 'preferences-page',
PREFERENCES_PAGE_SIDEBAR: 'preferences-page__sidebar',
PREFERENCES_PAGE_TOPBAR: 'preferences-page__topbar',
PREFERENCES_PAGE_CONTENT: 'preferences-page__content',
PREFERENCES_PAGE_TABS: 'preferences-page__tabs',
PREFERENCES_SIDEBAR: 'preferences-sidebar',
PREFERENCES_TOPBAR: 'preferences-topbar',
PREFERENCES_PAGE_INSIDE_CONTENT: 'preferences-page__inside-content',
PREFERENCES_PAGE_INSIDE_CONTENT_GENERAL: 'preferences-page__inside-content--general',
PREFERENCES_PAGE_INSIDE_CONTENT_USERS: 'preferences-page__inside-content--users',
PREFERENCES_PAGE_INSIDE_CONTENT_CURRENCIES: 'preferences-page__inside-content--currencies',
PREFERENCES_PAGE_INSIDE_CONTENT_ACCOUNTANT: 'preferences-page__inside-content--accountant',
FINANCIAL_REPORT_INSIDER: 'dashboard__insider--financial-report',
UNIVERSAL_SEARCH: 'universal-search',
UNIVERSAL_SEARCH_OMNIBAR: 'universal-search__omnibar',
UNIVERSAL_SEARCH_OVERLAY: 'universal-search-overlay',
UNIVERSAL_SEARCH_INPUT: 'universal-search__input',
UNIVERSAL_SEARCH_INPUT_RIGHT_ELEMENTS: 'universal-search-input-right-elements',
UNIVERSAL_SEARCH_TYPE_SELECT_OVERLAY: 'universal-search__type-select-overlay',
UNIVERSAL_SEARCH_TYPE_SELECT_BTN: 'universal-search__type-select-btn',
UNIVERSAL_SEARCH_FOOTER: 'universal-search__footer',
UNIVERSAL_SEARCH_ACTIONS: 'universal-search__actions',
UNIVERSAL_SEARCH_ACTION_SELECT: 'universal-search__action universal-search__action--select',
UNIVERSAL_SEARCH_ACTION_CLOSE: 'universal-search__action universal-search__action--close',
UNIVERSAL_SEARCH_ACTION_ARROWS: 'universal-search__action universal-search__action--arrows',
DIALOG_PDF_PREVIEW: 'dialog--pdf-preview-dialog',
...Classes,
CARD: 'card',
ALIGN_RIGHT: 'align-right',
FONT_BOLD: 'font-bold',
};
export {
CLASSES,
}

View File

@@ -0,0 +1,7 @@
import React from 'react';
import intl from 'react-intl-universal';
export default [
{ name: intl.get('customer'), path: 'customers' },
{ name: intl.get('vendor'), path: 'vendors' },
];

8
src/common/countries.js Normal file
View File

@@ -0,0 +1,8 @@
import intl from 'react-intl-universal';
export const getCountries = () => [
{
name: intl.get('libya'),
value: 'libya',
},
];

23
src/common/currencies.js Normal file
View File

@@ -0,0 +1,23 @@
import intl from 'react-intl-universal';
import currencies from 'js-money/lib/currency';
import { sortBy } from 'lodash';
export const getCurrencies = () => [
{ name: intl.get('us_dollar'), code: 'USD' },
{ name: intl.get('euro'), code: 'EUR' },
{ name: intl.get('libyan_diner'), code: 'LYD' },
];
export const getAllCurrenciesOptions = () => {
const codes = Object.keys(currencies);
const sortedCodes = sortBy(codes);
return sortedCodes.map((code) => {
const currency = currencies[code];
return {
key: code,
name: `${code} - ${currency.name}`,
};
});
};

7
src/common/dataTypes.js Normal file
View File

@@ -0,0 +1,7 @@
export const DATATYPES_LENGTH = {
STRING: 255,
TEXT: 65535,
INT_10: 4294967295,
DECIMAL_13_3: 9999999999.999,
DECIMAL_15_5: 999999999999.999,
};

View File

@@ -0,0 +1,41 @@
import moment from 'moment';
import intl from 'react-intl-universal';
export const getDateFormats =()=> [
{
id: 1,
name: intl.get('mm_dd_yy'),
label: `${moment().format('MM/DD/YYYY')}`,
value: 'mm/dd/yy',
},
{
id: 2,
name: intl.get('dd_mm_yy'),
label: `${moment().format('DD/MM/YYYY')}`,
value: 'dd/mm/yy',
},
{
id: 3,
name: intl.get('yy_mm_dd'),
label: `${moment().format('YYYY/MM/DD')}`,
value: 'yy/mm/dd',
},
{
id: 4,
name: intl.get('mm_dd_yy'),
label: `${moment().format('MM-DD-YYYY')}`,
value: 'mm-dd-yy',
},
{
id: 5,
name: intl.get('dd_mm_yy_'),
label: `${moment().format('DD-MM-YYYY')}`,
value: 'dd-mm-yy',
},
{
id: 6,
name: intl.get('yy_mm_dd_'),
label: `${moment().format('YYYY-MM-DD')}`,
value: 'yy-mm-dd',
},
];

12
src/common/drawers.js Normal file
View File

@@ -0,0 +1,12 @@
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',
};

19
src/common/errors.js Normal file
View File

@@ -0,0 +1,19 @@
export const ERROR = {
// Sales Estimates
ESTIMATE_NUMBER_IS_NOT_UNQIUE: 'ESTIMATE.NUMBER.IS.NOT.UNQIUE',
SALE_ESTIMATE_NO_IS_REQUIRED: 'SALE_ESTIMATE_NO_IS_REQUIRED',
// Sales Invoices
SALE_INVOICE_NUMBER_IS_EXISTS: 'SALE.INVOICE.NUMBER.IS.EXISTS',
SALE_INVOICE_NO_NOT_UNIQUE: 'SALE_INVOICE_NO_NOT_UNIQUE',
SALE_ESTIMATE_IS_ALREADY_CONVERTED_TO_INVOICE:
'SALE_ESTIMATE_IS_ALREADY_CONVERTED_TO_INVOICE',
// Sales Receipts
SALE_RECEIPT_NUMBER_NOT_UNIQUE: 'SALE_RECEIPT_NUMBER_NOT_UNIQUE',
SALE_RECEIPT_NO_IS_REQUIRED: 'SALE_RECEIPT_NO_IS_REQUIRED',
// Bills
BILL_NUMBER_EXISTS: 'BILL.NUMBER.EXISTS',
SALE_INVOICE_NO_IS_REQUIRED: 'SALE_INVOICE_NO_IS_REQUIRED',
};

View File

@@ -0,0 +1,52 @@
import intl from 'react-intl-universal';
export const getFiscalYear = () => [
{
name: `${intl.get('january')} - ${intl.get('december')}`,
key: 'january',
},
{
name: `${intl.get('february')} - ${intl.get('january')}`,
key: 'february',
},
{
name: `${intl.get('march')} - ${intl.get('february')}`,
key: 'march',
},
{
name: `${intl.get('april')} - ${intl.get('march')}`,
key: 'april',
},
{
name: `${intl.get('may')} - ${intl.get('april')}`,
key: 'may',
},
{
name: `${intl.get('june')} - ${intl.get('may')}`,
key: 'june',
},
{
name: `${intl.get('july')} - ${intl.get('june')}`,
key: 'july',
},
{
name: `${intl.get('august')} - ${intl.get('july')}`,
key: 'august',
},
{
name: `${intl.get('september')} - ${intl.get('august')}`,
key: 'september',
},
{
name: `${intl.get('october')} - ${intl.get('november')}`,
key: 'october',
},
{
name: `${intl.get('november')} - ${intl.get('october')}`,
key: 'november',
},
{
name: `${intl.get('december')} - ${intl.get('november')}`,
key: 'december',
},
]

View File

@@ -0,0 +1,121 @@
import React from 'react';
import { FormattedMessage as T } from 'components';
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',
},
{
title: <T id={'sales_estimates'} />,
description: <T id={'manage_your_sales_estimates_to_create_quotes'} />,
link: '/estimates',
},
{
title: <T id={'sales_receipts'} />,
description: <T id={'manage_sales_receipts_for_sales_that_get_paid'} />,
link: '/receipts',
},
{
title: <T id={'customers'} />,
description: <T id={'manage_the_customers_relations_with_customer'} />,
link: '/customers',
},
{
title: <T id={'customers_payments'} />,
description: (
<T id={'manage_payment_transactions_from_your_customers'} />
),
link: '/payment-receives',
},
],
},
];
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',
},
{
title: <T id={'vendors'} />,
description: (
<T id={'manage_the_vendors_relations_with_vendor_relations'} />
),
link: '/vendors',
},
{
title: <T id={'vendors_payments'} />,
description: <T id={'manage_payments_transactions_to_your_vendors'} />,
link: '/payment-mades',
},
],
},
];
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',
},
{
title: <T id={'manual_journal'}/>,
description:<T id={'manage_manual_journal_transactions_on_accounts'}/>,
link: '/manual-journals',
},
{
title: <T id={'expenses'}/>,
description:<T id={'track_your_indirect_expenses_under_specific_categories'}/>,
link: '/expenses',
},
{
title: <T id={'financial_statements'}/>,
description:<T id={'show_financial_reports_about_your_organization'}/>,
link: '/financial-reports',
},
],
},
];
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',
},
{
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',
},
],
},
];

View File

@@ -0,0 +1,105 @@
import React from 'react';
import intl from 'react-intl-universal';
export default [
{
shortcut_key: 'Shift + I',
description: intl.get('jump_to_the_invoices'),
},
{
shortcut_key: 'Shift + E',
description: intl.get('jump_to_the_estimates'),
},
{
shortcut_key: 'Shift + R',
description: intl.get('jump_to_the_receipts'),
},
{
shortcut_key: 'Shift + X',
description: intl.get('jump_to_the_expenses'),
},
{
shortcut_key: 'Shift + C',
description: intl.get('jump_to_the_customers'),
},
{
shortcut_key: 'Shift + V',
description: intl.get('jump_to_the_vendors'),
},
{
shortcut_key: 'Shift + A',
description: intl.get('jump_to_the_chart_of_accounts'),
},
{
shortcut_key: 'Shift + B',
description: intl.get('jump_to_the_bills'),
},
{
shortcut_key: 'Shift + M',
description: intl.get('jump_to_the_manual_journals'),
},
{
shortcut_key: 'Shift + W',
description: intl.get('jump_to_the_items'),
},
{
shortcut_key: 'Shift + 1',
description: intl.get('jump_to_the_balance_sheet'),
},
{
shortcut_key: 'Shift + 2',
description: intl.get('jump_to_the_profit_loss_sheet'),
},
{
shortcut_key: 'Shift + 3',
description: intl.get('jump_to_the_journal_sheet'),
},
{
shortcut_key: 'Shift + 4',
description: intl.get('jump_to_the_general_ledger_sheet'),
},
{
shortcut_key: 'Shift + 5',
description: intl.get('jump_to_the_trial_balance_sheet'),
},
{
shortcut_key: 'Ctrl + Shift + I ',
description: intl.get('create_a_new_invoice'),
},
{
shortcut_key: 'Ctrl + Shift + E ',
description: intl.get('create_a_new_estimate'),
},
{
shortcut_key: 'Ctrl + Shift + R ',
description: intl.get('create_a_new_receipt'),
},
{
shortcut_key: 'Ctrl + Shift + X ',
description: intl.get('create_a_new_expense'),
},
{
shortcut_key: 'Ctrl + Shift + C ',
description: intl.get('create_a_new_customer'),
},
{
shortcut_key: 'Ctrl + Shift + V ',
description: intl.get('create_a_new_vendor'),
},
{
shortcut_key: 'Ctrl + Shift + B ',
description: intl.get('create_a_new_bill'),
},
{
shortcut_key: 'Ctrl + Shift + M ',
description: intl.get('create_a_new_journal'),
},
{
shortcut_key: 'Ctrl + Shift + W ',
description: intl.get('create_a_new_item'),
},
{
shortcut_key: 'Ctrl + / ',
description: intl.get('close_and_open_sidebar'),
},
];

View File

@@ -0,0 +1,6 @@
import intl from 'react-intl-universal';
export const getLanguages = () => [
{ name: intl.get('english'), value: 'en' },
{ name: intl.get('arabic'), value: 'ar' },
];

View File

@@ -0,0 +1,21 @@
import intl from 'react-intl-universal';
export const moneyFormat = [
{ key: 'total', text: intl.get('total_rows') },
{ key: 'always', text: intl.get('always') },
{ key: 'none', text: intl.get('none') },
];
export const negativeFormat = [
{ key: 'parentheses', text: '($1000)' },
{ key: 'mines', text: '-$1000' },
];
export const decimalPlaces = [
{ text: '$1', key: 0 },
{ text: '$0.1', key: 1 },
{ text: '$0.01', key: 2 },
{ text: '$0.001', key: 3 },
{ text: '$0.0001', key: 4 },
{ text: '$0.00001', key: 5 },
];

View File

@@ -0,0 +1,10 @@
import intl from 'react-intl-universal';
export const getQuickNewActions = () => [
{ path: 'invoices/new', name: intl.get('sale_invoice') },
{ path: 'bills/new', name: intl.get('purchase_invoice') },
{ path: 'make-journal-entry', name: intl.get('manual_journal') },
{ path: 'expenses/new', name: intl.get('expense') },
{ path: 'customers/new', name: intl.get('customer') },
{ path: 'vendors/new', name: intl.get('vendor') },
];

View File

@@ -0,0 +1,16 @@
import intl from 'react-intl-universal';
export const getSetupWizardSteps = () => [
{
label: intl.get('setup.plan.plans'),
},
{
label: intl.get('setup.plan.getting_started'),
},
{
label: intl.get('setup.plan.initializing'),
},
{
label: intl.get('setup.plan.congrats'),
},
];

View File

@@ -0,0 +1,14 @@
export const RESOURCES_TYPES = {
INVOICE: 'invoice',
ESTIMATE: 'estimate',
RECEIPT: 'receipt',
PAYMENT_RECEIVE: 'payment_receive',
PAYMENT_MADE: 'payment_made',
CUSTOMER: 'customer',
VENDOR: 'vendor',
ITEM: 'item',
BILL: 'bill',
EXPENSE: 'expense',
MANUAL_JOURNAL: 'manual_journal',
ACCOUNT: 'account',
};

View File

@@ -0,0 +1,9 @@
// Subscription plans.
export const plans = [
];
// Payment methods.
export const paymentMethods = [
];

15
src/common/tables.js Normal file
View File

@@ -0,0 +1,15 @@
export const TABLES = {
ITEMS: 'items',
INVENTORY_ADJUSTMENTS: 'inventory_adjustment',
ESTIMATES: 'estimates',
INVOICES: 'invoices',
RECEIPTS: 'receipts',
PAYMENT_RECEIVES: 'payment_receives',
BILLS: 'bills',
PAYMENT_MADES: 'payment_mades',
CUSTOMER: 'customers',
VENDORS: 'vendors',
ACCOUNTS: 'accounts',
MANUAL_JOURNALS: 'manual_journal',
EXPENSES: 'expenses',
};

View File

@@ -0,0 +1,30 @@
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

@@ -0,0 +1,129 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import { MenuItemNestedText, FormattedMessage as T } from 'components';
import classNames from 'classnames';
import { filterAccountsByQuery } from './utils';
import { nestedArrayToflatten } from 'utils';
import { CLASSES } from 'common/classes';
export default function AccountsSelectList({
accounts,
initialAccountId,
selectedAccountId,
defaultSelectText = 'Select account',
onAccountSelected,
disabled = false,
popoverFill = false,
filterByParentTypes,
filterByTypes,
filterByNormal,
filterByRootTypes,
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],
);
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 onAccountSelect = useCallback(
(account) => {
setSelectedAccount({ ...account });
onAccountSelected && onAccountSelected(account);
},
[setSelectedAccount, onAccountSelected],
);
// Filters accounts items.
const filterAccountsPredicater = useCallback(
(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
);
}
},
[],
);
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={onAccountSelect}
disabled={disabled}
className={classNames('form-group--select-list', {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
>
<Button
disabled={disabled}
text={selectedAccount ? selectedAccount.name : defaultSelectText}
{...buttonProps}
/>
</Select>
);
}

View File

@@ -0,0 +1,135 @@
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 classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { MenuItemNestedText, FormattedMessage as T } from 'components';
import { filterAccountsByQuery } from './utils';
import { nestedArrayToflatten } from 'utils';
/**
* Accounts suggest field.
*/
export default function AccountsSuggestField({
accounts,
initialAccountId,
selectedAccountId,
defaultSelectText = intl.formatMessage({ id: 'select_account' }),
popoverFill = false,
onAccountSelected,
filterByParentTypes = [],
filterByTypes = [],
filterByNormal,
filterByRootTypes = [],
...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]);
// Filters accounts items.
const filterAccountsPredicater = useCallback(
(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
);
}
},
[],
);
// 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 handleInputValueRenderer = (inputValue) => {
if (inputValue) {
return inputValue.name.toString();
}
return '';
};
const onAccountSelect = useCallback(
(account) => {
setSelectedAccount({ ...account });
onAccountSelected && onAccountSelected(account);
},
[setSelectedAccount, onAccountSelected],
);
return (
<Suggest
items={filteredAccounts}
noResults={<MenuItem disabled={true} text={<T id={'no_accounts'} />} />}
itemRenderer={accountItem}
itemPredicate={filterAccountsPredicater}
onItemSelect={onAccountSelect}
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,
})}
{...suggestProps}
/>
);
}

View File

@@ -0,0 +1,48 @@
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

@@ -0,0 +1,13 @@
import * as Yup from 'yup';
export const getFilterDropdownSchema = () =>
Yup.object().shape({
conditions: Yup.array().of(
Yup.object().shape({
fieldKey: Yup.string(),
value: Yup.string().nullable(),
condition: Yup.string().nullable(),
comparator: Yup.string().nullable(),
}),
),
});

View File

@@ -0,0 +1,27 @@
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,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

@@ -0,0 +1,84 @@
import React, { createContext, useContext } from 'react';
import { get, keyBy } from 'lodash';
import { useFormikContext } from 'formik';
const AdvancedFilterContext = createContext({});
const FilterConditionContext = createContext({});
/**
* Advanced filter dropdown context provider.
*/
function AdvancedFilterDropdownProvider({
initialCondition,
fields,
...props
}) {
const fieldsByKey = keyBy(fields, 'key');
// Retrieve field meta by the given field key.
const getFieldMetaByKey = React.useCallback(
(key) => get(fieldsByKey, key),
[fieldsByKey],
);
// Provider payload.
const provider = { initialCondition, fields, fieldsByKey, getFieldMetaByKey };
return <AdvancedFilterContext.Provider value={provider} {...props} />;
}
/**
* Filter condition row context provider.
*/
function FilterConditionProvider({ conditionIndex, ...props }) {
const { setFieldValue, values } = useFormikContext();
const { getFieldMetaByKey } = useAdvancedFilterContext();
// Condition value path.
const conditionPath = `conditions[${conditionIndex}]`;
// Sets conditions value.
const setConditionValue = React.useCallback(
(field, value) => {
return setFieldValue(`${conditionPath}.${field}`, value);
},
[conditionPath, setFieldValue],
);
// Retrieve condition field value.
const getConditionValue = React.useCallback(
(field) => get(values, `${conditionPath}.${field}`),
[conditionPath, values],
);
// The current condition field meta.
const fieldMeta = React.useMemo(
() => getFieldMetaByKey(getConditionValue('fieldKey')),
[getFieldMetaByKey, getConditionValue],
);
// Retrieve the condition field path.
const getConditionFieldPath = React.useCallback(
(field) => `${conditionPath}.${field}`,
[conditionPath],
);
// Provider payload.
const provider = {
fieldMeta,
conditionIndex,
getConditionValue,
getConditionFieldPath,
setConditionValue,
};
return <FilterConditionContext.Provider value={provider} {...props} />;
}
const useFilterCondition = () => useContext(FilterConditionContext);
const useAdvancedFilterContext = () => useContext(AdvancedFilterContext);
export {
AdvancedFilterDropdownProvider,
FilterConditionProvider,
useAdvancedFilterContext,
useFilterCondition,
};

View File

@@ -0,0 +1,32 @@
import React from 'react';
import { Popover, PopoverInteractionKind, Position } from '@blueprintjs/core';
import { AdvancedFilterDropdown } from './AdvancedFilterDropdown';
/**
* Advanced filter popover.
*/
export function AdvancedFilterPopover({
popoverProps,
advancedFilterProps,
children,
}) {
return (
<Popover
minimal={true}
content={
<AdvancedFilterDropdown
{...advancedFilterProps}
/>
}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
canOutsideClickClose={true}
modifiers={{
offset: { offset: '0, 4' },
}}
{...popoverProps}
>
{children}
</Popover>
);
}

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 { 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={`Select an 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={'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('value')}
onChange={handleInputChange}
value={localValue}
inputRef={valueRef}
/>
</Choose.Otherwise>
</Choose>
);
}

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { useFormikContext } from 'formik';
import { debounce } from 'lodash';
const DEBOUNCE_MS = 100;
/**
* Advanced filter auto-save.
*/
export function useAdvancedFilterAutoSubmit() {
const { submitForm, values } = useFormikContext();
const [isSubmit, setIsSubmit] = React.useState(false);
const debouncedSubmit = React.useCallback(
debounce(() => {
return submitForm().then(() => setIsSubmit(true));
}, DEBOUNCE_MS),
[submitForm],
);
React.useEffect(() => debouncedSubmit, [debouncedSubmit, values]);
}

View File

@@ -0,0 +1,111 @@
import { ArrayHelpers } from 'formik';
import { IPopoverProps } from '@blueprintjs/core';
export type IResourceFieldType = 'text' | 'number' | 'enumeration' | 'boolean';
export interface IResourceField {
name: string;
key: string;
fieldType: IResourceFieldType;
}
export interface IAdvancedFilterDropdown {
fields: IResourceField[];
conditions?: IFilterRole[];
defaultFieldKey: string;
defaultComparator?: string;
defaultValue?: string;
defaultCondition?: string;
onFilterChange?: (filterRoles: IFilterRole[]) => void;
}
export interface IAdvancedFilterDropdownFooter {
onClick?: Function;
}
export interface IFilterFieldsField {
fields: IResourceField[];
}
export interface IFilterRole {
fieldKey: string;
comparator: string;
condition: string;
value: string;
}
export interface IAdvancedFilterContextProps {
initialCondition: IFilterRole;
fields: IResourceField[];
fieldsByKey: { [fieldKey: string]: IResourceField };
}
export interface IFilterConditionContextProps {
conditionIndex: number;
}
export interface IAdvancedFilterProviderProps {
initialCondition: IFilterRole;
fields: IResourceField[];
children: JSX.Element | JSX.Element[];
}
export interface IFilterConditionProviderProps {
conditionIndex: number;
children: JSX.Element | JSX.Element[];
}
export interface IFilterDropdownFormikValues {
conditions: IFilterRole[];
}
export type IAdvancedFilterDropdownConditionsProps = ArrayHelpers;
export interface IAdvancedFilterDropdownCondition {
conditionIndex: number;
onRemoveClick: Function;
}
export interface IFilterOption {
key: string;
label: string;
}
export interface IAdvancedFilterValueField {
fieldType: string;
value?: string;
key: string;
label: string;
options?: IFilterOption[];
onChange: Function;
}
export enum IFieldType {
TEXT = 'text',
NUMBER = 'number',
DATE = 'date',
ENUMERATION = 'enumeration',
BOOLEAN = 'boolean',
}
export interface IConditionTypeOption {
value: string;
label: string;
}
export interface IConditionOption {
label: string;
value: string;
text?: string;
}
export interface IAdvancedFilterPopover {
popoverProps?: IPopoverProps;
advancedFilterProps: IAdvancedFilterDropdown;
children: JSX.Element | JSX.Element[];
}
export interface IDynamicFilterCompatatorFieldProps {
dataType: string;
}

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

@@ -0,0 +1,17 @@
import React from 'react';
import clsx from 'classnames';
import Style from './style.module.scss';
export function Alert({ title, description, intent }) {
return (
<div
className={clsx(Style.root, {
[`${Style['root_' + intent]}`]: intent,
})}
>
{title && <h3 className={clsx(Style.title)}>{title}</h3>}
{description && <p class={clsx(Style.description)}>{description}</p>}
</div>
);
}

View File

@@ -0,0 +1,32 @@
.root {
border: 1px solid rgb(223, 227, 230);
padding: 12px;
border-radius: 6px;
margin-bottom: 20px;
&_danger {
border-color: rgb(249, 198, 198);
background: rgb(255, 248, 248);
.description {
color: #d95759;
}
.title {
color: rgb(205, 43, 49);
}
}
}
.title {
color: rgb(17, 24, 28);
margin-bottom: 4px;
font-size: 14px;
font-weight: 600;
}
.description {
color: rgb(104, 112, 118);
margin: 0;
}

51
src/components/App.js Normal file
View File

@@ -0,0 +1,51 @@
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 } from '../components';
import { queryConfig } from '../hooks/query/base'
/**
* Core application.
*/
export default function App() {
// Browser history.
const history = createBrowserHistory();
// Query client.
const queryClient = new QueryClient(queryConfig);
return (
<QueryClientProvider client={queryClient}>
<SplashScreen />
<AppIntlLoader>
<div className="App">
<Router history={history}>
<Switch>
<Route path={'/auth'} component={Authentication} />
<Route path={'/'}>
<PrivateRoute component={DashboardPrivatePages} />
</Route>
</Switch>
</Router>
<GlobalErrors />
</div>
</AppIntlLoader>
<ReactQueryDevtools initialIsOpen />
</QueryClientProvider>
);
}

View File

@@ -0,0 +1,123 @@
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 withDashboardActions from '../containers/Dashboard/withDashboardActions';
import withDashboard from '../containers/Dashboard/withDashboard';
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;
}
/**
* Application Intl loader.
*/
function AppIntlLoader({ appIntlIsLoading, setAppIntlIsLoading, children }) {
const [isLocalsLoading, setIsLocalsLoading] = React.useState(true);
const [isYupLoading, setIsYupLoading] = React.useState(true);
// 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);
React.useEffect(() => {
// Lodas the locales data file.
loadLocales(currentLocale)
.then((results) => {
return intl.init({
currentLocale,
locales: {
[currentLocale]: results,
},
});
})
.then(() => {
moment.locale(transformMomentLocale(currentLocale));
setIsLocalsLoading(false);
});
}, [currentLocale, setIsLocalsLoading]);
React.useEffect(() => {
loadYupLocales(currentLocale)
.then(({ locale }) => {
setLocale(locale);
setIsYupLoading(false);
})
.then(() => {});
}, [currentLocale]);
React.useEffect(() => {
if (!isLocalsLoading && !isYupLoading) {
setAppIntlIsLoading(false);
}
});
return (
<AppIntlProvider currentLocale={currentLocale} isRTL={isRTL}>
{appIntlIsLoading ? null : children}
</AppIntlProvider>
);
}
export default R.compose(
withDashboardActions,
withDashboard(({ appIntlIsLoading }) => ({ appIntlIsLoading })),
)(AppIntlLoader);

View File

@@ -0,0 +1,24 @@
import React, { createContext } from 'react';
const AppIntlContext = createContext();
/**
* Application intl provider.
*/
function AppIntlProvider({ currentLocale, isRTL, children }) {
const provider = {
currentLocale,
isRTL,
isLTR: !isRTL,
};
return (
<AppIntlContext.Provider value={provider}>
{children}
</AppIntlContext.Provider>
);
}
const useAppIntlContext = () => React.useContext(AppIntlContext);
export { AppIntlProvider, useAppIntlContext };

View File

@@ -0,0 +1,8 @@
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,64 @@
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 {AuthenticationBoot} from '../containers/Authentication/AuthenticationBoot';
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">
<AuthenticationBoot />
<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>
)}
</>
);
}

6
src/components/Card.js Normal file
View File

@@ -0,0 +1,6 @@
import React from 'react';
import classNames from 'classnames';
export default function Card({ className, children }) {
return <div className={classNames('card', className)}>{children}</div>;
}

View File

@@ -0,0 +1,59 @@
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

@@ -0,0 +1,23 @@
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

@@ -0,0 +1,107 @@
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,
selectedContactType,
defaultSelectText = <T id={'select_contact'} />,
onContactSelected,
popoverFill = false,
disabled = false,
buttonProps,
}) {
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 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 (
<Select
items={contacts}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
itemRenderer={handleContactRenderer}
itemPredicate={filterContacts}
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_')
}}
>
<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 '../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

@@ -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 '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

@@ -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

@@ -0,0 +1,56 @@
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

@@ -0,0 +1,82 @@
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

View File

@@ -0,0 +1,28 @@
import React from 'react';
import { useUser } from 'hooks/query';
import withAuthentication from '../../containers/Authentication/withAuthentication';
const AuthenticatedUserContext = React.createContext();
function AuthenticatedUserComponent({ authenticatedUserId, children }) {
const { data: user, ...restProps } = useUser(authenticatedUserId);
return (
<AuthenticatedUserContext.Provider
value={{
user,
...restProps,
}}
children={children}
/>
);
}
export const AuthenticatedUser = withAuthentication(
({ authenticatedUserId }) => ({
authenticatedUserId,
}),
)(AuthenticatedUserComponent);
export const useAuthenticatedUser = () =>
React.useContext(AuthenticatedUserContext);

View File

@@ -0,0 +1,18 @@
import React from 'react';
import classNames from 'classnames';
import { Icon } from 'components';
import 'style/components/BigcapitalLoading.scss';
/**
* Bigcapital logo loading.
*/
export default function BigcapitalLoading({ className }) {
return (
<div className={classNames('bigcapital-loading', className)}>
<div class="center">
<Icon icon="bigcapital" height={37} width={228} />
</div>
</div>
);
}

View File

@@ -0,0 +1,60 @@
import React from 'react';
import { Switch, Route } from 'react-router';
import 'style/pages/Dashboard/Dashboard.scss';
import Sidebar from 'components/Sidebar/Sidebar';
import DashboardContent from 'components/Dashboard/DashboardContent';
import DialogsContainer from 'components/DialogsContainer';
import PreferencesPage from 'components/Preferences/PreferencesPage';
import DashboardUniversalSearch from 'containers/UniversalSearch/DashboardUniversalSearch';
import DashboardSplitPane from 'components/Dashboard/DashboardSplitePane';
import GlobalHotkeys from './GlobalHotkeys';
import DashboardProvider from './DashboardProvider';
import DrawersContainer from 'components/DrawersContainer';
import EnsureSubscriptionIsActive from '../Guards/EnsureSubscriptionIsActive';
/**
* Dashboard preferences.
*/
function DashboardPreferences() {
return (
<EnsureSubscriptionIsActive>
<DashboardSplitPane>
<Sidebar />
<PreferencesPage />
</DashboardSplitPane>
</EnsureSubscriptionIsActive>
);
}
/**
* Dashboard other routes.
*/
function DashboardAnyPage() {
return (
<DashboardSplitPane>
<Sidebar />
<DashboardContent />
</DashboardSplitPane>
);
}
/**
* Dashboard page.
*/
export default function Dashboard() {
return (
<DashboardProvider>
<Switch>
<Route path="/preferences" component={DashboardPreferences} />
<Route path="/" component={DashboardAnyPage} />
</Switch>
<DashboardUniversalSearch />
<DialogsContainer />
<GlobalHotkeys />
<DrawersContainer />
</DashboardProvider>
);
}

View File

@@ -0,0 +1,68 @@
import React, { useMemo, useState } from 'react';
import classNames from 'classnames';
import {
Button,
Classes,
MenuItem,
Menu,
Popover,
PopoverInteractionKind,
Position,
Divider,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'components';
import { Icon } from 'components';
/**
* Dashboard action views list.
*/
export default function DashboardActionViewsList({
resourceName,
allMenuItem,
allMenuItemText,
views,
onChange,
}) {
const handleClickViewItem = (view) => {
onChange && onChange(view);
};
const viewsMenuItems = views.map((view) => (
<MenuItem onClick={() => handleClickViewItem(view)} text={view.name} />
));
const handleAllTabClick = () => {
handleClickViewItem(null);
};
const content = (
<Menu>
{allMenuItem && (
<>
<MenuItem
onClick={handleAllTabClick}
text={allMenuItemText || 'All'}
/>
<Divider />
</>
)}
{viewsMenuItems}
</Menu>
);
return (
<Popover
content={content}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--table-views')}
icon={<Icon icon="table-16" iconSize={16} />}
text={<T id={'table_views'} />}
rightIcon={'caret-down'}
/>
</Popover>
);
}

View File

@@ -0,0 +1,16 @@
import React from 'react';
import classnames from 'classnames';
import { Navbar } from '@blueprintjs/core';
export default function DashboardActionsBar({ children, name }) {
return (
<div
className={classnames({
'dashboard__actions-bar': true,
[`dashboard__actions-bar--${name}`]: !!name
})}
>
<Navbar className='navbar--dashboard-actions-bar'>{children}</Navbar>
</div>
);
}

View File

@@ -0,0 +1,39 @@
import React from 'react';
import withBreadcrumbs from 'react-router-breadcrumbs-hoc';
import { useHistory } from 'react-router-dom';
import { getDashboardRoutes } from 'routes/dashboard';
import { If, Icon } from 'components';
import { FormattedMessage as T } from 'components';
import withDashboard from 'containers/Dashboard/withDashboard';
import { compose } from 'utils';
function DashboardBackLink({ dashboardBackLink, breadcrumbs }) {
const history = useHistory();
const crumb = breadcrumbs[breadcrumbs.length - 2];
const handleClick = (event) => {
const url =
typeof dashboardBackLink === 'string'
? dashboardBackLink
: crumb.match.url;
history.push(url);
event.preventDefault();
};
return (
<If condition={dashboardBackLink && crumb}>
<div class="dashboard__back-link">
<a href="#no-link" onClick={handleClick}>
<Icon icon={'arrow-left'} iconSize={18} /> <T id={'back_to_list'} />
</a>
</div>
</If>
);
}
export default compose(
withBreadcrumbs([]),
withDashboard(({ dashboardBackLink }) => ({
dashboardBackLink,
})),
)(DashboardBackLink);

View File

@@ -0,0 +1,76 @@
import React from 'react';
import * as R from 'ramda';
import { useUser, useCurrentOrganization } from 'hooks/query';
import withAuthentication from '../../containers/Authentication/withAuthentication';
import withDashboardActions from '../../containers/Dashboard/withDashboardActions';
import { setCookie, getCookie } from '../../utils';
/**
* Dashboard async booting.
*/
function DashboardBootJSX({ setAppIsLoading, authenticatedUserId }) {
// Fetches the current user's organization.
const { isSuccess: isCurrentOrganizationSuccess, data: organization } =
useCurrentOrganization();
// Authenticated user.
const { isSuccess: isAuthUserSuccess, data: authUser } =
useUser(authenticatedUserId);
// Initial locale cookie value.
const localeCookie = getCookie('locale');
// Is the dashboard booted.
const isBooted = React.useRef(false);
// Syns the organization language with locale cookie.
React.useEffect(() => {
if (organization?.metadata?.language) {
setCookie('locale', organization.metadata.language);
}
}, [organization]);
React.useEffect(() => {
// Can't continue if the organization metadata is not loaded yet.
if (!organization?.metadata?.language) {
return;
}
// Can't continue if the organization is already booted.
if (isBooted.current) {
return;
}
// Reboot the application in case the initial locale not equal
// the current organization language.
if (localeCookie !== organization.metadata.language) {
window.location.reload();
}
}, [localeCookie, organization]);
React.useEffect(() => {
// Once the all requests complete change the app loading state.
if (
isAuthUserSuccess &&
isCurrentOrganizationSuccess &&
localeCookie === organization?.metadata?.language
) {
setAppIsLoading(false);
isBooted.current = true;
}
}, [
isAuthUserSuccess,
isCurrentOrganizationSuccess,
organization,
setAppIsLoading,
localeCookie,
]);
return null;
}
export const DashboardBoot = R.compose(
withAuthentication(({ authenticatedUserId }) => ({
authenticatedUserId,
})),
withDashboardActions,
)(DashboardBootJSX);

View File

@@ -0,0 +1,34 @@
import React from 'react';
import {
CollapsibleList,
MenuItem,
Classes,
Boundary,
} from '@blueprintjs/core';
import withBreadcrumbs from 'react-router-breadcrumbs-hoc';
import { getDashboardRoutes } from 'routes/dashboard';
import { useHistory } from 'react-router-dom';
function DashboardBreadcrumbs({ breadcrumbs }){
const history = useHistory();
return(
<CollapsibleList
className={Classes.BREADCRUMBS}
dropdownTarget={<span className={Classes.BREADCRUMBS_COLLAPSED} />}
collapseFrom={Boundary.START}
visibleItemCount={0}>
{
breadcrumbs.map(({ breadcrumb,match })=>{
return (<MenuItem
key={match.url}
icon={'folder-close'}
text={breadcrumb}
onClick={() => history.push(match.url) } />)
})
}
</CollapsibleList>
)
}
export default withBreadcrumbs([])(DashboardBreadcrumbs)

View File

@@ -0,0 +1,17 @@
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
// Dashboard card.
export default function DashboardCard({ children, page }) {
return (
<div
className={classNames(CLASSES.DASHBOARD_CARD, {
[CLASSES.DASHBOARD_CARD_PAGE]: page,
})}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,18 @@
import React from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import DashboardTopbar from 'components/Dashboard/DashboardTopbar';
import DashboardContentRoutes from 'components/Dashboard/DashboardContentRoute';
import DashboardFooter from 'components/Dashboard/DashboardFooter';
import DashboardErrorBoundary from './DashboardErrorBoundary';
export default React.forwardRef(({}, ref) => {
return (
<ErrorBoundary FallbackComponent={DashboardErrorBoundary}>
<div className="dashboard-content" id="dashboard" ref={ref}>
<DashboardTopbar />
<DashboardContentRoutes />
<DashboardFooter />
</div>
</ErrorBoundary>
);
});

View File

@@ -0,0 +1,59 @@
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import { getDashboardRoutes } from 'routes/dashboard';
import EnsureSubscriptionsIsActive from '../Guards/EnsureSubscriptionsIsActive';
import EnsureSubscriptionsIsInactive from '../Guards/EnsureSubscriptionsIsInactive';
import DashboardPage from './DashboardPage';
/**
* Dashboard inner route content.
*/
function DashboardContentRouteContent({ route }) {
const content = (
<DashboardPage
name={route.name}
Component={route.component}
pageTitle={route.pageTitle}
backLink={route.backLink}
hint={route.hint}
sidebarExpand={route.sidebarExpand}
pageType={route.pageType}
defaultSearchResource={route.defaultSearchResource}
/>
);
return route.subscriptionActive ? (
<EnsureSubscriptionsIsInactive
subscriptionTypes={route.subscriptionActive}
children={content}
redirectTo={'/billing'}
/>
) : route.subscriptionInactive ? (
<EnsureSubscriptionsIsActive
subscriptionTypes={route.subscriptionInactive}
children={content}
redirectTo={'/'}
/>
) : (
content
);
}
/**
* Dashboard content route.
*/
export default function DashboardContentRoute() {
const routes = getDashboardRoutes();
return (
<Route pathname="/">
<Switch>
{routes.map((route, index) => (
<Route exact={route.exact} key={index} path={`${route.path}`}>
<DashboardContentRouteContent route={route} />
</Route>
))}
</Switch>
</Route>
);
}

View File

@@ -0,0 +1,10 @@
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
/**
* Dashboard content table.
*/
export default function DashboardContentTable({ children }) {
return (<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>{ children }</div>)
}

View File

@@ -0,0 +1,12 @@
import React from 'react';
import { FormattedMessage as T, Icon } from 'components';
export default function DashboardErrorBoundary({}) {
return (
<div class="dashboard__error-boundary">
<h1><T id={'sorry_about_that_something_went_wrong'} /></h1>
<p><T id={'if_the_problem_stuck_please_contact_us_as_soon_as_possible'} /></p>
<Icon icon="bigcapital" height={30} width={160} />
</div>
)
}

View File

@@ -0,0 +1,26 @@
import React from 'react';
import classNames from 'classnames';
import intl from "react-intl-universal";
import { Classes, Button } from '@blueprintjs/core';
import { T, Icon } from 'components';
/**
* Dashboard advanced filter button.
*/
export function DashboardFilterButton({ conditionsCount }) {
return (
<Button
className={classNames(Classes.MINIMAL, 'button--filter', {
'has-active-filters': conditionsCount > 0,
})}
text={
conditionsCount > 0 ? (
intl.get('count_filters_applied', { count: conditionsCount })
) : (
<T id={'filter'} />
)
}
icon={<Icon icon="filter-16" iconSize={16} />}
/>
);
}

View File

@@ -0,0 +1,25 @@
import React from 'react';
import { getFooterLinks } from 'config/footerLinks';
import { For } from 'components';
function FooterLinkItem({ title, link }) {
return (
<div class="">
<a href={link} target="_blank" rel="noopener noreferrer">
{title}
</a>
</div>
);
}
export default function DashboardFooter() {
const footerLinks = getFooterLinks();
return (
<div class="dashboard__footer">
<div class="footer-links">
<For render={FooterLinkItem} of={footerLinks} />
</div>
</div>
);
}

View File

@@ -0,0 +1,23 @@
import React from 'react';
import classnames from 'classnames';
import LoadingIndicator from 'components/LoadingIndicator';
export default function DashboardInsider({
loading,
children,
name,
mount = false,
className,
}) {
return (
<div className={classnames({
'dashboard__insider': true,
'dashboard__insider--loading': loading,
[`dashboard__insider--${name}`]: !!name,
}, className)}>
<LoadingIndicator loading={loading} mount={mount}>
{ children }
</LoadingIndicator>
</div>
);
}

View File

@@ -0,0 +1,24 @@
import React from 'react';
import { Choose } from 'components';
import BigcapitalLoading from './BigcapitalLoading';
/**
* Dashboard loading indicator.
*/
export default function DashboardLoadingIndicator({
isLoading = false,
className,
children,
}) {
return (
<Choose>
<Choose.When condition={isLoading}>
<BigcapitalLoading />
</Choose.When>
<Choose.Otherwise>
{ children }
</Choose.Otherwise>
</Choose>
);
}

View File

@@ -0,0 +1,106 @@
import React, { useEffect, Suspense } from 'react';
// import { isUndefined } from 'lodash';
import { CLASSES } from 'common/classes';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
import { Spinner } from '@blueprintjs/core';
// import withUniversalSearch from '../../containers/UniversalSearch/withUniversalSearch';
import withUniversalSearchActions from '../../containers/UniversalSearch/withUniversalSearchActions';
/**
* Dashboard pages wrapper.
*/
function DashboardPage({
// #ownProps
pageTitle,
backLink,
sidebarExpand = true,
Component,
name,
hint,
defaultSearchResource,
// #withDashboardActions
changePageTitle,
setDashboardBackLink,
changePageHint,
toggleSidebarExpand,
// #withUniversalSearch
setResourceTypeUniversalSearch,
resetResourceTypeUniversalSearch,
}) {
// Hydrate the given page title.
useEffect(() => {
pageTitle && changePageTitle(pageTitle);
return () => {
pageTitle && changePageTitle('');
};
});
// Hydrate the given page hint.
useEffect(() => {
hint && changePageHint(hint);
return () => {
hint && changePageHint('');
};
}, [hint, changePageHint]);
// Hydrate the dashboard back link status.
useEffect(() => {
backLink && setDashboardBackLink(backLink);
return () => {
backLink && setDashboardBackLink(false);
};
}, [backLink, setDashboardBackLink]);
useEffect(() => {
const className = `page-${name}`;
name && document.body.classList.add(className);
return () => {
name && document.body.classList.remove(className);
};
}, [name]);
useEffect(() => {
toggleSidebarExpand(sidebarExpand);
}, [toggleSidebarExpand, sidebarExpand]);
useEffect(() => {
if (defaultSearchResource) {
setResourceTypeUniversalSearch(defaultSearchResource);
}
return () => {
resetResourceTypeUniversalSearch();
};
}, [
defaultSearchResource,
resetResourceTypeUniversalSearch,
setResourceTypeUniversalSearch,
]);
return (
<div className={CLASSES.DASHBOARD_PAGE}>
<Suspense
fallback={
<div class="dashboard__fallback-loading">
<Spinner size={40} value={null} />
</div>
}
>
<Component />
</Suspense>
</div>
);
}
export default compose(
withDashboardActions,
// withUniversalSearch,
withUniversalSearchActions,
)(DashboardPage);

View File

@@ -0,0 +1,12 @@
import React from 'react';
/**
* Dashboard page content.
*/
export default function DashboardPageContent({ children }) {
return (
<div class="dashboard__page-content">
{ children }
</div>
)
}

View File

@@ -0,0 +1,8 @@
import React from 'react';
/**
* Dashboard provider.
*/
export default function DashboardProvider({ children }) {
return children;
}

View File

@@ -0,0 +1,36 @@
import React from 'react';
import {
Button,
PopoverInteractionKind,
Popover,
Menu,
MenuItem,
MenuDivider,
Classes
} from '@blueprintjs/core';
import { Icon } from 'components';
export function DashboardRowsHeightButton() {
return (
<Popover
minimal={true}
content={
<Menu>
<MenuDivider title={'Rows height'} />
<MenuItem text="Compact" />
<MenuItem text="Medium" />
</Menu>
}
placement="bottom-start"
modifiers={{
offset: { offset: '0, 4' },
}}
interactionKind={PopoverInteractionKind.CLICK}
>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="rows-height" iconSize={16} />}
/>
</Popover>
);
}

View File

@@ -0,0 +1,44 @@
import React, { useState, useRef } from 'react';
import SplitPane from 'react-split-pane';
import { debounce } from 'lodash';
import withDashboard from 'containers/Dashboard/withDashboard';
import { compose } from 'utils';
function DashboardSplitPane({
sidebarExpended,
children
}) {
const initialSize = 180;
const [defaultSize, setDefaultSize] = useState(
parseInt(localStorage.getItem('dashboard-size'), 10) || initialSize,
);
const debounceSaveSize = useRef(
debounce((size) => {
localStorage.setItem('dashboard-size', size);
}, 500),
);
const handleChange = (size) => {
debounceSaveSize.current(size);
setDefaultSize(size);
}
return (
<SplitPane
allowResize={sidebarExpended}
split="vertical"
minSize={180}
maxSize={300}
defaultSize={sidebarExpended ? defaultSize : 50}
size={sidebarExpended ? defaultSize : 50}
onChange={handleChange}
className="primary"
>
{children}
</SplitPane>
);
}
export default compose(
withDashboard(({ sidebarExpended }) => ({ sidebarExpended }))
)(DashboardSplitPane);

View File

@@ -0,0 +1,197 @@
import React from 'react';
import { useHistory } from 'react-router';
import {
Navbar,
NavbarGroup,
NavbarDivider,
Button,
Classes,
Tooltip,
Position,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'components';
import DashboardTopbarUser from 'components/Dashboard/TopbarUser';
import DashboardBreadcrumbs from 'components/Dashboard/DashboardBreadcrumbs';
import DashboardBackLink from 'components/Dashboard/DashboardBackLink';
import { Icon, Hint, If } from 'components';
import withUniversalSearchActions from 'containers/UniversalSearch/withUniversalSearchActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withDashboard from 'containers/Dashboard/withDashboard';
import QuickNewDropdown from 'containers/QuickNewDropdown/QuickNewDropdown';
import { compose } from 'utils';
import withSubscriptions from '../../containers/Subscriptions/withSubscriptions';
function DashboardTopbarSubscriptionMessage() {
return (
<div class="dashboard__topbar-subscription-msg">
<span>
<T id={'dashboard.subscription_msg.period_over'} />
</span>
</div>
);
}
function DashboardHamburgerButton({ ...props }) {
return (
<Button minimal={true} {...props}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
role="img"
focusable="false"
>
<title>
<T id={'menu'} />
</title>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-miterlimit="5"
stroke-width="2"
d="M4 7h15M4 12h15M4 17h15"
></path>
</svg>
</Button>
);
}
/**
* Dashboard topbar.
*/
function DashboardTopbar({
// #withDashboard
pageTitle,
editViewId,
pageHint,
// #withDashboardActions
toggleSidebarExpand,
// #withDashboard
sidebarExpended,
// #withGlobalSearch
openGlobalSearch,
// #withSubscriptions
isSubscriptionActive,
isSubscriptionInactive,
}) {
const history = useHistory();
const handlerClickEditView = () => {
history.push(`/custom_views/${editViewId}/edit`);
};
const handleSidebarToggleBtn = () => {
toggleSidebarExpand();
};
return (
<div class="dashboard__topbar">
<div class="dashboard__topbar-left">
<div class="dashboard__topbar-sidebar-toggle">
<Tooltip
content={
!sidebarExpended ? (
<T id={'open_sidebar'} />
) : (
<T id={'close_sidebar'} />
)
}
position={Position.RIGHT}
>
<DashboardHamburgerButton onClick={handleSidebarToggleBtn} />
</Tooltip>
</div>
<div class="dashboard__title">
<h1>{pageTitle}</h1>
<If condition={pageHint}>
<div class="dashboard__hint">
<Hint content={pageHint} />
</div>
</If>
<If condition={editViewId}>
<Button
className={Classes.MINIMAL + ' button--view-edit'}
icon={<Icon icon="pen" iconSize={13} />}
onClick={handlerClickEditView}
/>
</If>
</div>
<div class="dashboard__breadcrumbs">
<DashboardBreadcrumbs />
</div>
<DashboardBackLink />
</div>
<div class="dashboard__topbar-right">
<If condition={isSubscriptionInactive}>
<DashboardTopbarSubscriptionMessage />
</If>
<Navbar class="dashboard__topbar-navbar">
<NavbarGroup>
<If condition={isSubscriptionActive}>
<Button
onClick={() => openGlobalSearch(true)}
className={Classes.MINIMAL}
icon={<Icon icon={'search-24'} iconSize={20} />}
text={<T id={'quick_find'} />}
/>
<QuickNewDropdown />
<Tooltip
content={<T id={'notifications'} />}
position={Position.BOTTOM}
>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'notification-24'} iconSize={20} />}
/>
</Tooltip>
</If>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'help-24'} iconSize={20} />}
text={<T id={'help'} />}
/>
<NavbarDivider />
</NavbarGroup>
</Navbar>
<div class="dashboard__topbar-user">
<DashboardTopbarUser />
</div>
</div>
</div>
);
}
export default compose(
withUniversalSearchActions,
withDashboard(({ pageTitle, pageHint, editViewId, sidebarExpended }) => ({
pageTitle,
editViewId,
sidebarExpended,
pageHint,
})),
withDashboardActions,
withSubscriptions(
({ isSubscriptionActive, isSubscriptionInactive }) => ({
isSubscriptionActive,
isSubscriptionInactive,
}),
'main',
),
)(DashboardTopbar);

View File

@@ -0,0 +1,103 @@
import React, { useRef, useState, useEffect } from 'react';
import { FormattedMessage as T } from 'components';
import PropTypes from 'prop-types';
import { Button, Tabs, Tab, Tooltip, Position } from '@blueprintjs/core';
import { useHistory } from 'react-router';
import { debounce } from 'lodash';
import { If, Icon } from 'components';
import { saveInvoke } from 'utils';
/**
* Dashboard views tabs.
*/
export default function DashboardViewsTabs({
initialViewSlug = 0,
currentViewSlug,
tabs,
defaultTabText = <T id={'all'} />,
allTab = true,
newViewTab = true,
resourceName,
onNewViewTabClick,
onChange,
OnThrottledChange,
throttleTime = 250,
}) {
const history = useHistory();
const [currentView, setCurrentView] = useState(initialViewSlug || 0);
useEffect(() => {
if (
typeof currentViewSlug !== 'undefined' &&
currentViewSlug !== currentView
) {
setCurrentView(currentViewSlug || 0);
}
}, [currentView, setCurrentView, currentViewSlug]);
const throttledOnChange = useRef(
debounce((viewId) => saveInvoke(OnThrottledChange, viewId), throttleTime),
);
// Trigger `onChange` and `onThrottledChange` events.
const triggerOnChange = (viewSlug) => {
const value = viewSlug === 0 ? null : viewSlug;
saveInvoke(onChange, value);
throttledOnChange.current(value);
};
// Handles click a new view.
const handleClickNewView = () => {
history.push(`/custom_views/${resourceName}/new`);
onNewViewTabClick && onNewViewTabClick();
};
// Handle tabs change.
const handleTabsChange = (viewSlug) => {
setCurrentView(viewSlug);
triggerOnChange(viewSlug);
};
return (
<div class="dashboard__views-tabs">
<Tabs
id="navbar"
large={true}
selectedTabId={currentView}
className="tabs--dashboard-views"
onChange={handleTabsChange}
animate={false}
>
{allTab && <Tab id={0} title={defaultTabText} />}
{tabs.map((tab) => (
<Tab id={tab.slug} title={tab.name} />
))}
<If condition={newViewTab}>
<Tooltip
content={<T id={'create_a_new_view'} />}
position={Position.RIGHT}
>
<Button
className="button--new-view"
icon={<Icon icon="plus" />}
onClick={handleClickNewView}
minimal={true}
/>
</Tooltip>
</If>
</Tabs>
</div>
);
}
DashboardViewsTabs.propTypes = {
tabs: PropTypes.array.isRequired,
allTab: PropTypes.bool,
newViewTab: PropTypes.bool,
onNewViewTabClick: PropTypes.func,
onChange: PropTypes.func,
OnThrottledChange: PropTypes.func,
throttleTime: PropTypes.number,
};

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useHistory } from 'react-router-dom';
import { getDashboardRoutes } from 'routes/dashboard';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
function GlobalHotkeys({
// #withDashboardActions
toggleSidebarExpend,
}) {
const history = useHistory();
const routes = getDashboardRoutes();
const globalHotkeys = routes
.filter(({ hotkey }) => hotkey)
.map(({ hotkey }) => hotkey)
.toString();
const handleSidebarToggleBtn = () => {
toggleSidebarExpend();
};
useHotkeys(
globalHotkeys,
(event, handle) => {
routes.map(({ path, hotkey }) => {
if (handle.key === hotkey) {
history.push(path);
}
});
},
[history],
);
useHotkeys('ctrl+/', (event, handle) => handleSidebarToggleBtn());
return <div></div>;
}
export default compose(withDashboardActions)(GlobalHotkeys);

View File

@@ -0,0 +1,37 @@
import React from 'react';
import { Switch, Route } from 'react-router';
import Dashboard from 'components/Dashboard/Dashboard';
import SetupWizardPage from 'containers/Setup/WizardSetupPage';
import EnsureOrganizationIsReady from 'components/Guards/EnsureOrganizationIsReady';
import EnsureOrganizationIsNotReady from 'components/Guards/EnsureOrganizationIsNotReady';
import { PrivatePagesProvider } from './PrivatePagesProvider';
import { DashboardBoot } from '../../components';
import 'style/pages/Dashboard/Dashboard.scss';
/**
* Dashboard inner private pages.
*/
export default function DashboardPrivatePages() {
return (
<PrivatePagesProvider>
<DashboardBoot />
<Switch>
<Route path={'/setup'}>
<EnsureOrganizationIsNotReady>
<SetupWizardPage />
</EnsureOrganizationIsNotReady>
</Route>
<Route path="/">
<EnsureOrganizationIsReady>
<Dashboard />
</EnsureOrganizationIsReady>
</Route>
</Switch>
</PrivatePagesProvider>
);
}

View File

@@ -0,0 +1,9 @@
import React from 'react';
import { AuthenticatedUser } from './AuthenticatedUser';
/**
* Private pages provider.
*/
export function PrivatePagesProvider({ children }) {
return <AuthenticatedUser>{children}</AuthenticatedUser>;
}

View File

@@ -0,0 +1,15 @@
import React from 'react';
import * as R from 'ramda';
import BigcapitalLoading from './BigcapitalLoading';
import withDashboard from '../../containers/Dashboard/withDashboard';
function SplashScreenComponent({ appIsLoading, appIntlIsLoading }) {
return appIsLoading || appIntlIsLoading ? <BigcapitalLoading /> : null;
}
export const SplashScreen = R.compose(
withDashboard(({ appIsLoading, appIntlIsLoading }) => ({
appIsLoading,
appIntlIsLoading,
})),
)(SplashScreenComponent);

View File

@@ -0,0 +1,89 @@
import React from 'react';
import { useHistory } from 'react-router-dom';
import {
Menu,
MenuItem,
MenuDivider,
Button,
Popover,
Position,
} from '@blueprintjs/core';
import { If, FormattedMessage as T } from 'components';
import { firstLettersArgs } from 'utils';
import { useAuthActions } from 'hooks/state';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
import withSubscriptions from '../../containers/Subscriptions/withSubscriptions';
import { useAuthenticatedUser } from './AuthenticatedUser';
function DashboardTopbarUser({
openDialog,
// #withSubscriptions
isSubscriptionActive
}) {
const history = useHistory();
const { setLogout } = useAuthActions();
// Retrieve authenticated user information.
const { user } = useAuthenticatedUser();
const onClickLogout = () => {
setLogout();
};
const onKeyboardShortcut = () => {
openDialog('keyboard-shortcuts');
};
return (
<Popover
content={
<Menu className={'menu--logged-user-dropdown'}>
<MenuItem
multiline={true}
className={'menu-item--profile'}
text={
<div>
<div class="person">
{user.first_name} {user.last_name}
</div>
<div class="org">
<T id="organization_id" />: {user.tenant_id}
</div>
</div>
}
/>
<MenuDivider />
<If condition={isSubscriptionActive}>
<MenuItem
text={<T id={'keyboard_shortcuts'} />}
onClick={onKeyboardShortcut}
/>
<MenuItem
text={<T id={'preferences'} />}
onClick={() => history.push('/preferences')}
/>
</If>
<MenuItem text={<T id={'logout'} />} onClick={onClickLogout} />
</Menu>
}
position={Position.BOTTOM}
>
<Button>
<div className="user-text">
{firstLettersArgs(user.first_name, user.last_name)}
</div>
</Button>
</Popover>
);
}
export default compose(
withDialogActions,
withSubscriptions(
({ isSubscriptionActive }) => ({ isSubscriptionActive }),
'main',
),
)(DashboardTopbarUser);

View File

@@ -0,0 +1,4 @@
export * from './SplashScreen';
export * from './DashboardBoot';

226
src/components/DataTable.js Normal file
View File

@@ -0,0 +1,226 @@
import React, { useEffect, useRef } from 'react';
import {
useTable,
useExpanded,
useRowSelect,
usePagination,
useResizeColumns,
useSortBy,
useFlexLayout,
useAsyncDebounce,
} from 'react-table';
import { useSticky } from 'react-table-sticky';
import { useUpdateEffect } from 'hooks';
import { saveInvoke } from 'utils';
import 'style/components/DataTable/DataTable.scss';
import TableNoResultsRow from './Datatable/TableNoResultsRow';
import TableLoadingRow from './Datatable/TableLoading';
import TableHeader from './Datatable/TableHeader';
import TablePage from './Datatable/TablePage';
import TableFooter from './Datatable/TableFooter';
import TableRow from './Datatable/TableRow';
import TableRows from './Datatable/TableRows';
import TableCell from './Datatable/TableCell';
import TableTBody from './Datatable/TableTBody';
import TableContext from './Datatable/TableContext';
import TablePagination from './Datatable/TablePagination';
import TableWrapper from './Datatable/TableWrapper';
import TableIndeterminateCheckboxRow from './Datatable/TableIndeterminateCheckboxRow';
import TableIndeterminateCheckboxHeader from './Datatable/TableIndeterminateCheckboxHeader';
import { useResizeObserver } from './Datatable/utils';
/**
* Datatable component.
*/
export default function DataTable(props) {
const {
columns,
data,
onFetchData,
onSelectedRowsChange,
manualSortBy = false,
manualPagination = true,
selectionColumn = false,
expandSubRows = true,
expanded = {},
rowClassNames,
payload,
expandable = false,
noInitialFetch = false,
pagesCount: controlledPageCount,
// Pagination props.
initialPageIndex = 0,
initialPageSize = 10,
updateDebounceTime = 200,
selectionColumnWidth = 42,
autoResetPage,
autoResetExpanded,
autoResetGroupBy,
autoResetSelectedRows,
autoResetSortBy,
autoResetFilters,
autoResetRowState,
// Components
TableHeaderRenderer,
TablePageRenderer,
TableWrapperRenderer,
TableTBodyRenderer,
TablePaginationRenderer,
TableFooterRenderer,
onColumnResizing,
initialColumnsWidths,
...restProps
} = props;
const selectionColumnObj = {
id: 'selection',
disableResizing: true,
minWidth: selectionColumnWidth,
width: selectionColumnWidth,
maxWidth: selectionColumnWidth,
skeletonWidthMin: 100,
// The header can use the table's getToggleAllRowsSelectedProps method
// to render a checkbox
Header: TableIndeterminateCheckboxHeader,
// The cell can use the individual row's getToggleRowSelectedProps method
// to the render a checkbox
Cell: TableIndeterminateCheckboxRow,
className: 'selection',
...(typeof selectionColumn === 'object' ? selectionColumn : {}),
};
const table = useTable(
{
columns,
data,
initialState: {
pageIndex: initialPageIndex,
pageSize: initialPageSize,
expanded,
columnResizing: {
columnWidths: initialColumnsWidths || {},
},
},
manualPagination,
pageCount: controlledPageCount,
getSubRows: (row) => row.children,
manualSortBy,
expandSubRows,
payload,
autoResetPage,
autoResetExpanded,
autoResetGroupBy,
autoResetSelectedRows,
autoResetSortBy,
autoResetFilters,
autoResetRowState,
...restProps,
},
useSortBy,
useExpanded,
useResizeColumns,
useFlexLayout,
useSticky,
usePagination,
useRowSelect,
(hooks) => {
hooks.visibleColumns.push((columns) => [
// Let's make a column for selection
...(selectionColumn ? [selectionColumnObj] : []),
...columns,
]);
},
);
const {
selectedFlatRows,
state: { pageIndex, pageSize, sortBy, selectedRowIds },
} = table;
const isInitialMount = useRef(noInitialFetch);
const onFetchDataDebounced = useAsyncDebounce((...args) => {
saveInvoke(onFetchData, ...args);
}, updateDebounceTime);
// When these table states change, fetch new data!
useEffect(() => {
if (isInitialMount.current) {
isInitialMount.current = false;
} else {
onFetchDataDebounced({ pageIndex, pageSize, sortBy });
}
}, [pageIndex, pageSize, sortBy, onFetchDataDebounced]);
useUpdateEffect(() => {
saveInvoke(onSelectedRowsChange, selectedFlatRows);
}, [selectedRowIds, onSelectedRowsChange]);
// Column resizing observer.
useResizeObserver(table.state, (current, columnWidth, columnsResizing) => {
onColumnResizing && onColumnResizing(current, columnWidth, columnsResizing);
});
return (
<TableContext.Provider value={{ table, props }}>
<TableWrapperRenderer>
<TableHeaderRenderer />
<TableTBodyRenderer>
<TablePageRenderer />
</TableTBodyRenderer>
<TableFooterRenderer />
</TableWrapperRenderer>
<TablePaginationRenderer />
</TableContext.Provider>
);
}
DataTable.defaultProps = {
pagination: false,
spinnerProps: { size: 30 },
expandToggleColumn: 1,
expandColumnSpace: 0.8,
autoResetPage: true,
autoResetExpanded: true,
autoResetGroupBy: true,
autoResetSelectedRows: true,
autoResetSortBy: true,
autoResetFilters: true,
autoResetRowState: true,
TableHeaderRenderer: TableHeader,
TableFooterRenderer: TableFooter,
TableLoadingRenderer: TableLoadingRow,
TablePageRenderer: TablePage,
TableRowsRenderer: TableRows,
TableRowRenderer: TableRow,
TableCellRenderer: TableCell,
TableWrapperRenderer: TableWrapper,
TableTBodyRenderer: TableTBody,
TablePaginationRenderer: TablePagination,
TableNoResultsRowRenderer: TableNoResultsRow,
noResults: '',
payload: {},
};

View File

@@ -0,0 +1,72 @@
import React, { useRef, useCallback, useMemo } from 'react';
import classNames from 'classnames';
import { useCellAutoFocus } from 'hooks';
import intl from 'react-intl-universal';
import AccountsSuggestField from 'components/AccountsSuggestField';
// import AccountsSelectList from 'components/AccountsSelectList';
import { FormGroup, Classes, Intent } from '@blueprintjs/core';
/**
* Account cell renderer.
*/
export default function AccountCellRenderer({
column: {
id,
accountsDataProp,
filterAccountsByRootTypes,
filterAccountsByTypes,
},
row: { index, original },
cell: { value: initialValue },
payload: {
accounts: defaultAccounts,
updateData,
errors,
autoFocus,
...restPayloadProps
},
}) {
const accountRef = useRef();
useCellAutoFocus(accountRef, autoFocus, id, index);
const handleAccountSelected = useCallback(
(account) => {
updateData(index, id, account.id);
},
[updateData, index, id],
);
const error = errors?.[index]?.[id];
const accounts = useMemo(
() => restPayloadProps[accountsDataProp] || defaultAccounts,
[restPayloadProps, defaultAccounts, accountsDataProp],
);
return (
<FormGroup
intent={error ? Intent.DANGER : null}
className={classNames(
'form-group--select-list',
'form-group--account',
Classes.FILL,
)}
>
<AccountsSuggestField
accounts={accounts}
onAccountSelected={handleAccountSelected}
selectedAccountId={initialValue}
filterByRootTypes={filterAccountsByRootTypes}
filterByTypes={filterAccountsByTypes}
inputProps={{
inputRef: (ref) => (accountRef.current = ref),
placeholder: intl.get('search'),
}}
openOnKeyDown={true}
blurOnSelectClose={false}
/>
</FormGroup>
);
}

View File

@@ -0,0 +1,48 @@
import React from 'react';
import classNames from 'classnames';
import { get } from 'lodash';
import { Classes, Checkbox, FormGroup, Intent } from '@blueprintjs/core';
const CheckboxEditableCell = ({
row: { index, original },
column: { id, disabledAccessor, checkboxProps },
cell: { value: initialValue },
payload,
}) => {
const [value, setValue] = React.useState(initialValue);
const onChange = (e) => {
const newValue = e.target.checked;
setValue(newValue);
payload.updateData(index, id, newValue);
};
React.useEffect(() => {
setValue(initialValue);
}, [initialValue]);
const error = payload.errors?.[index]?.[id];
// Detarmines whether the checkbox is disabled.
const disabled = disabledAccessor ? get(original, disabledAccessor) : false;
return (
<FormGroup
intent={error ? Intent.DANGER : null}
className={classNames(Classes.FILL)}
>
<Checkbox
value={value}
onChange={onChange}
checked={initialValue}
disabled={disabled}
minimal={true}
className="ml2"
{...checkboxProps}
/>
</FormGroup>
);
};
export default CheckboxEditableCell;

View File

@@ -0,0 +1,39 @@
import React, { useCallback } from 'react';
import { FormGroup, Intent, Classes } from '@blueprintjs/core';
import classNames from 'classnames';
import { ContactSelecetList } from 'components';
import ContactsSuggestField from 'components/ContactsSuggestField';
export default function ContactsListCellRenderer({
column: { id },
row: { index, original },
cell: { value },
payload: { contacts, updateData, errors },
}) {
const handleContactSelected = useCallback(
(contact) => {
updateData(index, 'contact_id', contact.id);
},
[updateData, index, id],
);
const error = errors?.[index]?.[id];
return (
<FormGroup
intent={error ? Intent.DANGER : null}
className={classNames(
'form-group--select-list',
'form-group--contacts-list',
Classes.FILL,
)}
>
<ContactsSuggestField
contactsList={contacts}
onContactSelected={handleContactSelected}
selectedContactId={original?.contact_id}
selectedContactType={original?.contact_type}
/>
</FormGroup>
);
}

View File

@@ -0,0 +1,20 @@
import React, { useState, useEffect } from 'react';
export const DivFieldCell = ({ cell: { value: initialValue } }) => {
const [value, setValue] = useState(initialValue);
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return <div>${value}</div>;
};
export const EmptyDiv = ({ cell: { value: initialValue } }) => {
const [value, setValue] = useState(initialValue);
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return <div>{value}</div>;
};

View File

@@ -0,0 +1,40 @@
import React, { useState, useEffect } from 'react';
import classNames from 'classnames';
import { Classes, InputGroup, FormGroup, Intent } from '@blueprintjs/core';
const InputEditableCell = ({
row: { index },
column: { id },
cell: { value: initialValue },
payload,
}) => {
const [value, setValue] = useState(initialValue);
const onChange = (e) => {
setValue(e.target.value);
};
const onBlur = () => {
payload.updateData(index, id, value);
};
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
const error = payload.errors?.[index]?.[id];
return (
<FormGroup
intent={error ? Intent.DANGER : null}
className={classNames(Classes.FILL)}
>
<InputGroup
value={value}
onChange={onChange}
onBlur={onBlur}
fill={true}
/>
</FormGroup>
);
};
export default InputEditableCell;

View File

@@ -0,0 +1,51 @@
import React, { useCallback, useRef } from 'react';
// import ItemsListField from 'components/ItemsListField';
import ItemsSuggestField from 'components/ItemsSuggestField';
import classNames from 'classnames';
import { FormGroup, Classes, Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import { useCellAutoFocus } from 'hooks';
export default function ItemsListCell({
column: { id, filterSellable, filterPurchasable },
row: { index },
cell: { value: initialValue },
payload: { items, updateData, errors, autoFocus },
}) {
const fieldRef = useRef();
// Auto-focus the items list input field.
useCellAutoFocus(fieldRef, autoFocus, id, index);
const handleItemSelected = useCallback(
(item) => {
updateData(index, id, item.id);
},
[updateData, index, id],
);
const error = errors?.[index]?.[id];
return (
<FormGroup
intent={error ? Intent.DANGER : null}
className={classNames('form-group--select-list', Classes.FILL)}
>
<ItemsSuggestField
items={items}
onItemSelected={handleItemSelected}
selectedItemId={initialValue}
sellable={filterSellable}
purchasable={filterPurchasable}
inputProps={{
inputRef: (ref) => (fieldRef.current = ref),
placeholder: intl.get('enter_an_item'),
}}
openOnKeyDown={true}
blurOnSelectClose={false}
/>
</FormGroup>
);
}

View File

@@ -0,0 +1,51 @@
import React, { useCallback, useState, useEffect } from 'react';
import { FormGroup, Intent } from '@blueprintjs/core';
import { MoneyInputGroup } from 'components';
import { CLASSES } from 'common/classes';
// Input form cell renderer.
const MoneyFieldCellRenderer = ({
row: { index, moneyInputGroupProps = {} },
column: { id },
cell: { value: initialValue },
payload: { errors, updateData },
}) => {
const [value, setValue] = useState(initialValue);
const handleFieldChange = useCallback((value) => {
setValue(value);
}, [setValue]);
function isNumeric(data) {
return (
!isNaN(parseFloat(data)) && isFinite(data) && data.constructor !== Array
);
}
const handleFieldBlur = () => {
const updateValue = isNumeric(value) ? parseFloat(value) : value;
updateData(index, id, updateValue);
};
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
const error = errors?.[index]?.[id];
return (
<FormGroup
intent={error ? Intent.DANGER : null}
className={CLASSES.FILL}>
<MoneyInputGroup
value={value}
// prefix={'$'}
onChange={handleFieldChange}
onBlur={handleFieldBlur}
{...moneyInputGroupProps}
/>
</FormGroup>
);
};
export default MoneyFieldCellRenderer;

View File

@@ -0,0 +1,43 @@
import React, { useState, useEffect } from 'react';
import { FormGroup, NumericInput, Intent } from '@blueprintjs/core';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
/**
* Numeric input table cell.
*/
export default function NumericInputCell({
row: { index },
column: { id },
cell: { value: initialValue },
payload,
}) {
const [value, setValue] = useState(initialValue);
const handleValueChange = (newValue) => {
setValue(newValue);
};
const onBlur = () => {
payload.updateData(index, id, value);
};
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
const error = payload.errors?.[index]?.[id];
return (
<FormGroup
intent={error ? Intent.DANGER : null}
className={classNames(CLASSES.FILL)}
>
<NumericInput
value={value}
onValueChange={handleValueChange}
onBlur={onBlur}
fill={true}
buttonPosition={"none"}
/>
</FormGroup>
);
}

View File

@@ -0,0 +1,35 @@
import React, { useCallback } from 'react';
import PaymentReceiveListField from 'components/PaymentReceiveListField';
import classNames from 'classnames';
import { FormGroup, Classes, Intent } from '@blueprintjs/core';
function PaymentReceiveListFieldCell({
column: { id },
row: { index },
cell: { value: initialValue },
payload: { invoices, updateData, errors },
}) {
const handleInvoicesSelected = useCallback(
(_item) => {
updateData(index, id, _item.id);
},
[updateData, index, id],
);
const error = errors?.[index]?.[id];
return (
<FormGroup
intent={error ? Intent.DANGER : null}
className={classNames('form-group--selcet-list', Classes.FILL)}
>
<PaymentReceiveListField
invoices={invoices}
onInvoiceSelected={handleInvoicesSelected}
selectedInvoiceId={initialValue}
/>
</FormGroup>
);
}
export default PaymentReceiveListFieldCell;

View File

@@ -0,0 +1,41 @@
import React, { useCallback, useState, useEffect } from 'react';
import { FormGroup, Intent } from '@blueprintjs/core';
import { MoneyInputGroup } from 'components';
const PercentFieldCell = ({
cell: { value: initialValue },
row: { index },
column: { id },
payload: { errors, updateData },
}) => {
const [value, setValue] = useState(initialValue);
const handleBlurChange = (newValue) => {
const parsedValue = newValue === '' || newValue === undefined
? '' : parseInt(newValue, 10);
updateData(index, id, parsedValue);
};
const handleChange = useCallback((value) => {
setValue(value);
}, [setValue]);
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
const error = errors?.[index]?.[id];
return (
<FormGroup intent={error ? Intent.DANGER : null}>
<MoneyInputGroup
prefix={'%'}
value={value}
onChange={handleChange}
onBlurValue={handleBlurChange}
/>
</FormGroup>
);
};
export default PercentFieldCell;

View File

@@ -0,0 +1,22 @@
import AccountsListFieldCell from './AccountsListFieldCell';
import MoneyFieldCell from './MoneyFieldCell';
import InputGroupCell from './InputGroupCell';
import ContactsListFieldCell from './ContactsListFieldCell';
import ItemsListCell from './ItemsListCell';
import PercentFieldCell from './PercentFieldCell';
import { DivFieldCell, EmptyDiv } from './DivFieldCell';
import NumericInputCell from './NumericInputCell';
import CheckBoxFieldCell from './CheckBoxFieldCell'
export {
AccountsListFieldCell,
MoneyFieldCell,
InputGroupCell,
ContactsListFieldCell,
ItemsListCell,
PercentFieldCell,
DivFieldCell,
EmptyDiv,
NumericInputCell,
CheckBoxFieldCell
};

View File

@@ -0,0 +1,5 @@
import React from 'react';
export function CellTextSpan({ cell: { value } }) {
return (<span class="cell-text">{ value }</span>)
}

View File

@@ -0,0 +1,36 @@
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { DataTable, If } from 'components';
import 'style/components/DataTable/DataTableEditable.scss';
/**
* Editable datatable.
*/
export default function DatatableEditable({
totalRow = false,
actions,
name,
className,
...tableProps
}) {
return (
<div
className={classNames(
CLASSES.DATATABLE_EDITOR,
{
[`${CLASSES.DATATABLE_EDITOR}--${name}`]: name,
},
className,
)}
>
<DataTable {...tableProps} />
<If condition={actions}>
<div className={classNames(CLASSES.DATATABLE_EDITOR_ACTIONS)}>
{actions}
</div>
</If>
</div>
);
}

View File

@@ -0,0 +1,11 @@
export default function TableBody({}) {
return (
<ScrollSyncPane>
<div {...getTableBodyProps()} className="tbody">
<div class="tbody-inner" style={{ minWidth: totalColumnsWidth }}></div>
</div>
</ScrollSyncPane>
);
}

View File

@@ -0,0 +1,100 @@
import React, { useContext } from 'react';
import classNames from 'classnames';
import { If } from 'components';
import { Skeleton } from 'components';
import { useAppIntlContext } from 'components/AppIntlProvider';
import TableContext from './TableContext';
import { saveInvoke } from 'utils';
import { isCellLoading } from './utils';
/**
* Table cell.
*/
export default function TableCell({ cell, row, index }) {
const { index: rowIndex, depth, getToggleRowExpandedProps, isExpanded } = row;
const {
props: {
expandToggleColumn,
expandColumnSpace,
expandable,
cellsLoading,
cellsLoadingCoords,
onCellClick,
},
} = useContext(TableContext);
const isExpandColumn = expandToggleColumn === index;
const { skeletonWidthMax = 100, skeletonWidthMin = 40 } = {};
// Application intl context.
const { isRTL } = useAppIntlContext();
// Detarmines whether the current cell is loading.
const cellLoading = isCellLoading(
cellsLoading,
cellsLoadingCoords,
rowIndex,
cell.column.id,
);
if (cellLoading) {
return (
<div
{...cell.getCellProps({
className: classNames(cell.column.className, 'td'),
})}
>
<Skeleton minWidth={skeletonWidthMin} maxWidth={skeletonWidthMax} />
</div>
);
}
// Handle cell click action.
const handleCellClick = (event) => {
saveInvoke(onCellClick, cell, event);
};
return (
<div
{...cell.getCellProps({
className: classNames(cell.column.className, 'td', {
'is-text-overview': cell.column.textOverview,
'clickable': cell.column.clickable,
'align-right': cell.column.align === 'right',
}),
onClick: handleCellClick,
})}
>
<div
className={classNames(
{
'text-overview': cell.column.textOverview,
},
'cell-inner',
)}
style={{
[isRTL ? 'paddingRight' : 'paddingLeft']:
isExpandColumn && expandable
? `${depth * expandColumnSpace}rem`
: '',
}}
>
{
// Use the row.canExpand and row.getToggleRowExpandedProps prop getter
// to build the toggle for expanding a row
}
<If condition={cell.row.canExpand && expandable && isExpandColumn}>
<span {...getToggleRowExpandedProps({ className: 'expand-toggle' })}>
<span
className={classNames({
'arrow-down': isExpanded,
'arrow-right': !isExpanded,
})}
/>
</span>
</If>
{cell.render('Cell')}
</div>
</div>
);
}

View File

@@ -0,0 +1,3 @@
import { createContext } from 'react';
export default createContext();

View File

@@ -0,0 +1,20 @@
import React, { memo } from 'react';
import TableCell from './TableCell';
function TableFastCell({ cell, row, index }) {
return <TableCell cell={cell} row={row} index={index} />;
}
export default memo(TableFastCell, (prevProps, nextProps) => {
if (
prevProps.row.canExpand === nextProps.row.canExpand &&
prevProps.row.isExpanded === nextProps.row.isExpanded &&
prevProps.cell.value === nextProps.cell.value &&
prevProps.cell.maxWidth === nextProps.cell.maxWidth &&
prevProps.cell.width === nextProps.cell.width
) {
return true;
} else {
return false;
}
});

View File

@@ -0,0 +1,36 @@
import React, { useContext } from 'react';
import classNames from 'classnames';
import TableContext from './TableContext';
/**
* Table footer.
*/
export default function TableFooter() {
const {
props: { footer },
table: { footerGroups },
} = useContext(TableContext);
// Can't contiunue if the footer is disabled.
if (!footer) { return null; }
return (
<div class="tfooter">
{footerGroups.map((group) => (
<div {...group.getFooterGroupProps({ className: 'tr' })}>
{group.headers.map((column) => (
<div
{...column.getFooterProps({
className: classNames(column.className || '', 'td'),
})}
>
<div className={'cell-inner'}>
{column.render('Footer')}
</div>
</div>
))}
</div>
))}
</div>
);
}

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