diff --git a/package.json b/package.json index bd109512b..38afbca86 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "@blueprintjs/select": "^3.11.2", "@blueprintjs/table": "^3.8.3", "@blueprintjs/timezone": "^3.6.2", + "@casl/ability": "^5.4.3", + "@casl/react": "^2.3.0", "@reduxjs/toolkit": "^1.2.5", "@sentry/react": "^6.13.2", "@sentry/tracing": "^6.13.2", diff --git a/src/common/abilityOption.js b/src/common/abilityOption.js new file mode 100644 index 000000000..1a5625ca3 --- /dev/null +++ b/src/common/abilityOption.js @@ -0,0 +1,161 @@ +export const AbilitySubject = { + Item: 'Item', + InventoryAdjustment: 'InventoryAdjustment', + Estimate: 'SaleEstimate', + Invoice: 'SaleInvoice', + Receipt: 'SaleReceipt', + PaymentReceive: 'PaymentReceive', + Bill: 'Bill', + PaymentMade: 'PaymentMade', + Customer: 'Customer', + Vendor: 'Vendor', + Account: 'Account', + ManualJournal: 'ManualJournal', + Expense: 'Expense', + Cashflow: 'Cashflow', + Report: 'Report', + Preferences: 'Preferences', + ExchangeRate: 'ExchangeRate', + SubscriptionBilling: 'SubscriptionBilling', +}; + +export const ItemAction = { + View: 'View', + Create: 'Create', + Edit: 'Edit', + Delete: 'Delete', +}; + +export const InventoryAdjustmentAction = { + Create: 'Create', + Edit: 'Edit', + View: 'View', + Delete: 'Delete', +}; + +export const SaleEstimateAction = { + View: 'View', + Create: 'Create', + Edit: 'Edit', + Delete: 'Delete', + NotifyBySms: 'NotifyBySms', +}; + +export const SaleInvoiceAction = { + View: 'View', + Create: 'Create', + Edit: 'Edit', + Delete: 'Delete', + Writeoff: 'Writeoff', + NotifyBySms: 'NotifyBySms', +}; + +export const SaleReceiptAction = { + View: 'View', + Create: 'Create', + Edit: 'Edit', + Delete: 'Delete', + NotifyBySms: 'NotifyBySms', +}; + +export const PaymentReceiveAction = { + View: 'View', + Create: 'Create', + Edit: 'Edit', + Delete: 'Delete', + NotifyBySms: 'NotifyBySms', +}; + +export const BillAction = { + View: 'View', + Create: 'Create', + Edit: 'Edit', + Delete: 'Delete', + NotifyBySms: 'NotifyBySms', +}; + +export const PaymentMadeAction = { + View: 'View', + Create: 'Create', + Edit: 'Edit', + Delete: 'Delete', +}; + +export const CustomerAction = { + View: 'View', + Create: 'Create', + Edit: 'Edit', + Delete: 'Delete', +}; + +export const VendorAction = { + View: 'View', + Create: 'Create', + Edit: 'Edit', + Delete: 'Delete', +}; + +export const AccountAction = { + View: 'View', + Create: 'Create', + Edit: 'Edit', + Delete: 'Delete', + TransactionsLocking: 'TransactionsLocking', +}; + +export const ManualJournalAction = { + View: 'View', + Create: 'Create', + Edit: 'Edit', + Delete: 'Delete', + TransactionLocking: 'TransactionLocking', +}; + +export const ExpenseAction = { + View: 'View', + Create: 'Create', + Edit: 'Edit', + Delete: 'Delete', +}; + +export const CashflowAction = { + View: 'View', + Create: 'Create', + Delete: 'Delete', +}; + +export const ReportsAction = { + ALL: 'all', + READ_BALANCE_SHEET: 'read-balance-sheet', + READ_TRIAL_BALANCE_SHEET: 'read-trial-balance-sheet', + READ_PROFIT_LOSS: 'read-profit-loss', + READ_JOURNAL: 'read-journal', + READ_GENERAL_LEDGET: 'read-general-ledger', + READ_CASHFLOW: 'read-cashflow', + READ_AR_AGING_SUMMARY: 'read-ar-aging-summary', + READ_AP_AGING_SUMMARY: 'read-ap-aging-summary', + READ_PURCHASES_BY_ITEMS: 'read-purchases-by-items', + READ_SALES_BY_ITEMS: 'read-sales-by-items', + READ_CUSTOMERS_TRANSACTIONS: 'read-customers-transactions', + READ_VENDORS_TRANSACTIONS: 'read-vendors-transactions', + READ_CUSTOMERS_SUMMARY_BALANCE: 'read-customers-summary-balance', + READ_VENDORS_SUMMARY_BALANCE: 'read-vendors-summary-balance', + READ_INVENTORY_VALUATION_SUMMARY: 'read-inventory-valuation-summary', + READ_INVENTORY_ITEM_DETAILS: 'read-inventory-item-details', + READ_CASHFLOW_ACCOUNT_TRANSACTION: 'read-cashflow-account-transactions', +}; + +export const PreferencesAbility = { + Mutate: 'Mutate', +}; + +export const ExchangeRateAbility = { + View: 'view', + Create: 'create', + Delete: 'delete', +}; + +export const SubscriptionBillingAbility = { + View: 'view', + Payment: 'payment', +}; diff --git a/src/common/homepageOptions.js b/src/common/homepageOptions.js index 82972db3a..beb9ab623 100644 --- a/src/common/homepageOptions.js +++ b/src/common/homepageOptions.js @@ -1,5 +1,21 @@ import React from 'react'; import { FormattedMessage as T } from 'components'; +import { + SaleInvoiceAction, + SaleEstimateAction, + AbilitySubject, + SaleReceiptAction, + CustomerAction, + PaymentReceiveAction, + BillAction, + VendorAction, + PaymentMadeAction, + AccountAction, + ManualJournalAction, + ExpenseAction, + ItemAction, + ReportsAction, +} from '../common/abilityOption'; export const accountsReceivable = [ { @@ -9,21 +25,29 @@ export const accountsReceivable = [ title: , description: , link: '/invoices', + subject: AbilitySubject.Invoice, + ability: SaleInvoiceAction.View, }, { title: , description: , link: '/estimates', + subject: AbilitySubject.Estimate, + ability: SaleEstimateAction.View, }, { title: , description: , link: '/receipts', + subject: AbilitySubject.Receipt, + ability: SaleReceiptAction.View, }, { title: , description: , link: '/customers', + subject: AbilitySubject.Customer, + ability: CustomerAction.View, }, { title: , @@ -31,6 +55,8 @@ export const accountsReceivable = [ ), link: '/payment-receives', + subject: AbilitySubject.PaymentReceive, + ability: PaymentReceiveAction.View, }, ], }, @@ -46,6 +72,8 @@ export const accountsPayable = [ ), link: '/bills', + subject: AbilitySubject.Bill, + ability: BillAction.View, }, { title: , @@ -53,11 +81,15 @@ export const accountsPayable = [ ), link: '/vendors', + subject: AbilitySubject.Vendor, + ability: VendorAction.View, }, { title: , description: , link: '/payment-mades', + subject: AbilitySubject.PaymentMade, + ability: PaymentMadeAction.View, }, ], }, @@ -77,21 +109,35 @@ export const financialAccounting = [ /> ), link: '/accounts', + subject: AbilitySubject.Account, + ability: AccountAction.View, }, { - title: , - description:, + title: , + description: ( + + ), link: '/manual-journals', + subject: AbilitySubject.ManualJournal, + ability: ManualJournalAction.View, }, { - title: , - description:, + title: , + description: ( + + ), link: '/expenses', + subject: AbilitySubject.Expense, + ability: ExpenseAction.View, }, { - title: , - description:, + title: , + description: ( + + ), link: '/financial-reports', + subject: AbilitySubject.Report, + ability: ReportsAction.ALL, }, ], }, @@ -102,19 +148,27 @@ export const productsServices = [ sectionTitle: , shortcuts: [ { - title: , - description:, + title: , + description: ( + + ), link: '/items', + subject: AbilitySubject.Item, + ability: ItemAction.View, }, { - title: , - description:, + title: , + description: , link: 'items/categories', }, { - title: , - description: , + title: , + description: ( + + ), link: '/inventory-adjustments', + subject: AbilitySubject.InventoryAdjustment, + ability: SaleInvoiceAction.View, }, ], }, diff --git a/src/common/keyboardShortcutsOptions.js b/src/common/keyboardShortcutsOptions.js index 9a904e09b..cd026dedf 100644 --- a/src/common/keyboardShortcutsOptions.js +++ b/src/common/keyboardShortcutsOptions.js @@ -1,110 +1,228 @@ -import React from 'react'; import intl from 'react-intl-universal'; +import { + AbilitySubject, + AccountAction, + BillAction, + CashflowAction, + CustomerAction, + ExpenseAction, + ItemAction, + ManualJournalAction, + ReportsAction, + SaleEstimateAction, + SaleInvoiceAction, + SaleReceiptAction, + VendorAction, +} from './abilityOption'; export default [ { shortcut_key: 'Shift + I', description: intl.get('jump_to_the_invoices'), + permission: { + ability: SaleInvoiceAction.View, + subject: AbilitySubject.Invoice, + }, }, { shortcut_key: 'Shift + E', description: intl.get('jump_to_the_estimates'), + permission: { + ability: SaleEstimateAction.View, + subject: AbilitySubject.Estimate, + }, }, { shortcut_key: 'Shift + R', description: intl.get('jump_to_the_receipts'), + permission: { + ability: SaleReceiptAction.View, + subject: AbilitySubject.Receipt, + }, }, { shortcut_key: 'Shift + X', description: intl.get('jump_to_the_expenses'), + permission: { + ability: ExpenseAction.View, + subject: AbilitySubject.Expense, + }, }, { shortcut_key: 'Shift + C', description: intl.get('jump_to_the_customers'), + permission: { + ability: CustomerAction.View, + subject: AbilitySubject.Customer, + }, }, { shortcut_key: 'Shift + V', description: intl.get('jump_to_the_vendors'), + permission: { + ability: VendorAction.View, + subject: AbilitySubject.Vendor, + }, }, { shortcut_key: 'Shift + A', description: intl.get('jump_to_the_chart_of_accounts'), + permission: { + ability: AccountAction.View, + subject: AbilitySubject.Account, + }, }, { shortcut_key: 'Shift + B', description: intl.get('jump_to_the_bills'), + permission: { + ability: BillAction.View, + subject: AbilitySubject.Bill, + }, }, { shortcut_key: 'Shift + M', description: intl.get('jump_to_the_manual_journals'), + permission: { + ability: ManualJournalAction.View, + subject: AbilitySubject.ManualJournal, + }, }, { shortcut_key: 'Shift + W', description: intl.get('jump_to_the_items'), + permission: { + ability: ItemAction.View, + subject: AbilitySubject.Item, + }, }, { shortcut_key: 'Shift + D', description: intl.get('jump_to_the_add_money_in'), + permission: { + ability: CashflowAction.Create, + subject: AbilitySubject.Cashflow, + }, }, { shortcut_key: 'Shift + Q', description: intl.get('jump_to_the_add_money_out'), + permission: { + ability: CashflowAction.Create, + subject: AbilitySubject.Cashflow, + }, }, { shortcut_key: 'Shift + 1', description: intl.get('jump_to_the_balance_sheet'), + permission: { + ability: ReportsAction.READ_BALANCE_SHEET, + subject: AbilitySubject.Report, + }, }, { shortcut_key: 'Shift + 2', description: intl.get('jump_to_the_profit_loss_sheet'), + permission: { + ability: ReportsAction.READ_PROFIT_LOSS, + subject: AbilitySubject.Report, + }, }, { shortcut_key: 'Shift + 3', description: intl.get('jump_to_the_journal_sheet'), + permission: { + ability: ReportsAction.READ_JOURNAL, + subject: AbilitySubject.Report, + }, }, { shortcut_key: 'Shift + 4', description: intl.get('jump_to_the_general_ledger_sheet'), + permission: { + ability: ReportsAction.READ_GENERAL_LEDGET, + subject: AbilitySubject.Report, + }, }, { shortcut_key: 'Shift + 5', description: intl.get('jump_to_the_trial_balance_sheet'), + permission: { + ability: ReportsAction.READ_TRIAL_BALANCE_SHEET, + subject: AbilitySubject.Report, + }, }, { shortcut_key: 'Ctrl + Shift + I ', description: intl.get('create_a_new_invoice'), + permission: { + ability: SaleInvoiceAction.Create, + subject: AbilitySubject.Invoice, + }, }, { shortcut_key: 'Ctrl + Shift + E ', description: intl.get('create_a_new_estimate'), + permission: { + ability: SaleEstimateAction.Create, + subject: AbilitySubject.Estimate, + }, }, { shortcut_key: 'Ctrl + Shift + R ', description: intl.get('create_a_new_receipt'), + permission: { + ability: SaleReceiptAction.Create, + subject: AbilitySubject.Receipt, + }, }, { shortcut_key: 'Ctrl + Shift + X ', description: intl.get('create_a_new_expense'), + permission: { + ability: ExpenseAction.Create, + subject: AbilitySubject.Expense, + }, }, { shortcut_key: 'Ctrl + Shift + C ', description: intl.get('create_a_new_customer'), + permission: { + ability: CustomerAction.Create, + subject: AbilitySubject.Customer, + }, }, { shortcut_key: 'Ctrl + Shift + V ', description: intl.get('create_a_new_vendor'), + permission: { + ability: VendorAction.Create, + subject: AbilitySubject.Vendor, + }, }, { shortcut_key: 'Ctrl + Shift + B ', description: intl.get('create_a_new_bill'), + permission: { + ability: BillAction.Create, + subject: AbilitySubject.Bill, + }, }, { shortcut_key: 'Ctrl + Shift + M ', description: intl.get('create_a_new_journal'), + permission: { + ability: ManualJournalAction.Create, + subject: AbilitySubject.ManualJournal, + }, }, { shortcut_key: 'Ctrl + Shift + W ', description: intl.get('create_a_new_item'), + permission: { + ability: ItemAction.Create, + subject: AbilitySubject.Item, + }, }, { shortcut_key: 'Ctrl + / ', diff --git a/src/common/quickNewOptions.js b/src/common/quickNewOptions.js index 74b9fece8..ba4f9604b 100644 --- a/src/common/quickNewOptions.js +++ b/src/common/quickNewOptions.js @@ -1,10 +1,71 @@ import intl from 'react-intl-universal'; +import { + AbilitySubject, + SaleInvoiceAction, + CustomerAction, + VendorAction, + ManualJournalAction, + ExpenseAction, +} from '../common/abilityOption'; +import { useAbilitiesFilter } from '../hooks'; export const getQuickNewActions = () => [ - { path: 'invoices/new', name: intl.get('sale_invoice') }, - { 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') }, + { + path: 'invoices/new', + name: intl.get('sale_invoice'), + permission: { + subject: AbilitySubject.Invoice, + ability: SaleInvoiceAction.Create, + }, + }, + { + path: 'bills/new', + name: intl.get('purchase_invoice'), + permission: { + subject: AbilitySubject.Invoice, + ability: SaleInvoiceAction.Create, + }, + }, + { + path: 'make-journal-entry', + name: intl.get('manual_journal'), + permission: { + subject: AbilitySubject.ManualJournal, + ability: ManualJournalAction.Create, + }, + }, + { + path: 'expenses/new', + name: intl.get('expense'), + permission: { + subject: AbilitySubject.Expense, + ability: ExpenseAction.Create, + }, + }, + { + path: 'customers/new', + name: intl.get('customer'), + permission: { + subject: AbilitySubject.Customer, + ability: CustomerAction.Create, + }, + }, + { + path: 'vendors/new', + name: intl.get('vendor'), + permission: { + subject: AbilitySubject.Vendor, + ability: VendorAction.Vendor, + }, + }, ]; + +/** + * Retrieve the dashboard quick new menu items. + */ +export const useGetQuickNewMenu = () => { + const quickNewMenu = getQuickNewActions(); + const abilitiesFilter = useAbilitiesFilter(); + + return abilitiesFilter(quickNewMenu); +}; diff --git a/src/components/Abilities.js b/src/components/Abilities.js deleted file mode 100644 index 152ea01b0..000000000 --- a/src/components/Abilities.js +++ /dev/null @@ -1,10 +0,0 @@ -import { AbilityBuilder, defineAbility } from '@casl/ability'; -import { createContextualCan } from '@casl/react'; -import { createContext } from 'react'; - -export const AbilityContext = createContext(); -export const Can = createContextualCan(AbilityContext.Consumer); - -export const ability = defineAbility((can, cannot) => { - cannot('Item', 'create'); -}); diff --git a/src/components/Can.js b/src/components/Can.js deleted file mode 100644 index 9ba7fe434..000000000 --- a/src/components/Can.js +++ /dev/null @@ -1,4 +0,0 @@ -import { createCanBoundTo } from '@casl/react'; -import ability from '../components/Config/ability'; - -export default createCanBoundTo(ability); \ No newline at end of file diff --git a/src/components/Config/ability.js b/src/components/Config/ability.js deleted file mode 100644 index 72342959f..000000000 --- a/src/components/Config/ability.js +++ /dev/null @@ -1,10 +0,0 @@ -import { AbilityBuilder } from '@casl/ability'; -// import { AbilitySubject, ItemAbility } from '../../common/abilityOption'; - -export function defineAbilitiesFor(role) { - const { rules, can } = new AbilityBuilder(); - - can('create', 'Item'); - - return new Ability(rules); -} diff --git a/src/components/Dashboard/AuthenticatedUser.js b/src/components/Dashboard/AuthenticatedUser.js deleted file mode 100644 index d65d58685..000000000 --- a/src/components/Dashboard/AuthenticatedUser.js +++ /dev/null @@ -1,28 +0,0 @@ -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 ( - - ); -} - -export const AuthenticatedUser = withAuthentication( - ({ authenticatedUserId }) => ({ - authenticatedUserId, - }), -)(AuthenticatedUserComponent); - -export const useAuthenticatedUser = () => - React.useContext(AuthenticatedUserContext); diff --git a/src/components/Dashboard/DashboardAbilityProvider.js b/src/components/Dashboard/DashboardAbilityProvider.js new file mode 100644 index 000000000..37de0ced2 --- /dev/null +++ b/src/components/Dashboard/DashboardAbilityProvider.js @@ -0,0 +1,25 @@ +import React from 'react'; +import { Ability } from '@casl/ability'; +import { createContextualCan } from '@casl/react'; +import { useDashboardMeta } from '../../hooks/query'; + +export const AbilityContext = React.createContext(); +export const Can = createContextualCan(AbilityContext.Consumer); + +/** + * Dashboard ability provider. + */ +export function DashboardAbilityProvider({ children }) { + const { + data: { abilities }, + } = useDashboardMeta(); + + // Ability instance. + const ability = new Ability(abilities); + + return ( + + {children} + + ); +} diff --git a/src/components/Dashboard/DashboardBoot.js b/src/components/Dashboard/DashboardBoot.js index 81eadb658..6e3d967fb 100644 --- a/src/components/Dashboard/DashboardBoot.js +++ b/src/components/Dashboard/DashboardBoot.js @@ -1,18 +1,53 @@ import React from 'react'; -import * as R from 'ramda'; - -import { useUser, useCurrentOrganization } from '../../hooks/query'; +import { + useAuthenticatedAccount, + useCurrentOrganization, + useDashboardMeta, +} from '../../hooks/query'; import { useSplashLoading } from '../../hooks/state'; import { useWatch, useWatchImmediate, useWhen } from '../../hooks'; - -import withAuthentication from '../../containers/Authentication/withAuthentication'; - import { setCookie, getCookie } from '../../utils'; /** - * Dashboard async booting. + * Dashboard meta async booting. */ -function DashboardBootJSX({ authenticatedUserId }) { +export function useDashboardMetaBoot() { + const { + data: dashboardMeta, + isLoading: isDashboardMetaLoading, + isSuccess: isDashboardMetaSuccess, + } = useDashboardMeta({ + keepPreviousData: true, + }); + const [startLoading, stopLoading] = useSplashLoading(); + + useWatchImmediate((value) => { + value && startLoading(); + }, isDashboardMetaLoading); + + useWatchImmediate(() => { + isDashboardMetaSuccess && stopLoading(); + }, isDashboardMetaSuccess); + + return { + isLoading: isDashboardMetaLoading, + }; +} + +/** + * Dashboard async booting. + * @returns {{ isLoading: boolean }} + */ +export function useDashboardBoot() { + const { isLoading } = useDashboardMetaBoot(); + + return { isLoading }; +} + +/** + * Application async booting. + */ +export function useApplicationBoot() { // Fetches the current user's organization. const { isSuccess: isCurrentOrganizationSuccess, @@ -22,7 +57,7 @@ function DashboardBootJSX({ authenticatedUserId }) { // Authenticated user. const { isSuccess: isAuthUserSuccess, isLoading: isAuthUserLoading } = - useUser(authenticatedUserId); + useAuthenticatedAccount(); // Initial locale cookie value. const localeCookie = getCookie('locale'); @@ -86,11 +121,8 @@ function DashboardBootJSX({ authenticatedUserId }) { isBooted.current = true; }, ); - return null; -} -export const DashboardBoot = R.compose( - withAuthentication(({ authenticatedUserId }) => ({ - authenticatedUserId, - })), -)(DashboardBootJSX); + return { + isLoading: isOrgLoading || isAuthUserLoading, + }; +} diff --git a/src/components/Dashboard/DashboardProvider.js b/src/components/Dashboard/DashboardProvider.js index 5a5fce966..68c8e5654 100644 --- a/src/components/Dashboard/DashboardProvider.js +++ b/src/components/Dashboard/DashboardProvider.js @@ -1,8 +1,16 @@ import React from 'react'; +import { DashboardAbilityProvider } from '../../components'; +import { useDashboardBoot } from './DashboardBoot'; /** * Dashboard provider. */ export default function DashboardProvider({ children }) { - return children; + const { isLoading } = useDashboardBoot(); + + // Avoid display any dashboard component before complete booting. + if (isLoading) { + return null; + } + return {children}; } diff --git a/src/components/Dashboard/DashboardTopbar.js b/src/components/Dashboard/DashboardTopbar.js index 075d2fe01..141dda575 100644 --- a/src/components/Dashboard/DashboardTopbar.js +++ b/src/components/Dashboard/DashboardTopbar.js @@ -23,6 +23,7 @@ import withDashboard from 'containers/Dashboard/withDashboard'; import QuickNewDropdown from 'containers/QuickNewDropdown/QuickNewDropdown'; import { compose } from 'utils'; import withSubscriptions from '../../containers/Subscriptions/withSubscriptions'; +import { useGetUniversalSearchTypeOptions } from '../../containers/UniversalSearch/utils'; function DashboardTopbarSubscriptionMessage() { return ( @@ -142,11 +143,8 @@ function DashboardTopbar({ - + + - + + } /> diff --git a/src/containers/Accounting/JournalsLanding/components.js b/src/containers/Accounting/JournalsLanding/components.js index af986e522..fab711e72 100644 --- a/src/containers/Accounting/JournalsLanding/components.js +++ b/src/containers/Accounting/JournalsLanding/components.js @@ -13,7 +13,18 @@ import { } from '@blueprintjs/core'; import intl from 'react-intl-universal'; -import { FormattedMessage as T, Choose, Money, If, Icon } from 'components'; +import { + Can, + FormattedMessage as T, + Choose, + Money, + If, + Icon, +} from 'components'; +import { + ManualJournalAction, + AbilitySubject, +} from '../../../common/abilityOption'; import { safeCallback } from 'utils'; /** @@ -150,25 +161,31 @@ export const ActionsMenu = ({ text={intl.get('view_details')} onClick={safeCallback(onViewDetails, original)} /> - - + + + + } + text={intl.get('publish_journal')} + onClick={safeCallback(onPublish, original)} + /> + + + } - text={intl.get('publish_journal')} - onClick={safeCallback(onPublish, original)} + icon={} + text={intl.get('edit_journal')} + onClick={safeCallback(onEdit, original)} /> - - } - text={intl.get('edit_journal')} - onClick={safeCallback(onEdit, original)} - /> - } - intent={Intent.DANGER} - onClick={safeCallback(onDelete, original)} - /> + + + } + intent={Intent.DANGER} + onClick={safeCallback(onDelete, original)} + /> + ); }; diff --git a/src/containers/Accounting/ManualJournalUniversalSearch.js b/src/containers/Accounting/ManualJournalUniversalSearch.js index 4cde8bafe..6c4a6266a 100644 --- a/src/containers/Accounting/ManualJournalUniversalSearch.js +++ b/src/containers/Accounting/ManualJournalUniversalSearch.js @@ -1,6 +1,10 @@ import intl from 'react-intl-universal'; import { RESOURCES_TYPES } from 'common/resourcesTypes'; import withDrawerActions from '../Drawer/withDrawerActions'; +import { + AbilitySubject, + ManualJournalAction, +} from '../../common/abilityOption'; /** * Universal search manual journal item select action. @@ -44,4 +48,8 @@ export const universalSearchJournalBind = () => ({ optionItemLabel: intl.get('manual_journals'), selectItemAction: JournalUniversalSearchSelectAction, itemSelect: manualJournalsToSearch, + permission: { + ability: ManualJournalAction.View, + subject: AbilitySubject.ManualJournal, + }, }); diff --git a/src/containers/Accounts/AccountUniversalSearch.js b/src/containers/Accounts/AccountUniversalSearch.js index a1af66eb3..afa8744d5 100644 --- a/src/containers/Accounts/AccountUniversalSearch.js +++ b/src/containers/Accounts/AccountUniversalSearch.js @@ -1,7 +1,10 @@ import intl from 'react-intl-universal'; -import { RESOURCES_TYPES } from '../../common/resourcesTypes'; + import withDrawerActions from '../Drawer/withDrawerActions'; +import { AbilitySubject, AccountAction } from '../../common/abilityOption'; +import { RESOURCES_TYPES } from '../../common/resourcesTypes'; + function AccountUniversalSearchItemSelectComponent({ // #ownProps resourceType, @@ -42,4 +45,8 @@ export const universalSearchAccountBind = () => ({ optionItemLabel: intl.get('accounts'), selectItemAction: AccountUniversalSearchItemSelect, itemSelect: accountToSearch, + permission: { + ability: AccountAction.View, + subject: AbilitySubject.Account, + }, }); diff --git a/src/containers/Accounts/AccountsActionsBar.js b/src/containers/Accounts/AccountsActionsBar.js index e96d69c8a..d77f309fc 100644 --- a/src/containers/Accounts/AccountsActionsBar.js +++ b/src/containers/Accounts/AccountsActionsBar.js @@ -15,6 +15,7 @@ import { FormattedMessage as T } from 'components'; import { AdvancedFilterPopover, If, + Can, DashboardActionViewsList, DashboardFilterButton, DashboardRowsHeightButton, @@ -30,6 +31,8 @@ import withAlertActions from 'containers/Alert/withAlertActions'; import withAccountsTableActions from './withAccountsTableActions'; import withSettings from '../Settings/withSettings'; import withSettingsActions from '../Settings/withSettingsActions'; +import { AccountAction, AbilitySubject } from '../../common/abilityOption'; + import { compose } from 'utils'; /** @@ -116,13 +119,14 @@ function AccountsActionsBar({ onChange={handleTabChange} /> - - + + - + + } /> diff --git a/src/containers/Customers/CustomersLanding/components.js b/src/containers/Customers/CustomersLanding/components.js index 0cce24139..9d878bbf4 100644 --- a/src/containers/Customers/CustomersLanding/components.js +++ b/src/containers/Customers/CustomersLanding/components.js @@ -4,7 +4,8 @@ import clsx from 'classnames'; import intl from 'react-intl-universal'; -import { Icon, Money, If, AvaterCell } from 'components'; +import { Can, Icon, Money, If, AvaterCell } from 'components'; +import { CustomerAction, AbilitySubject } from '../../../common/abilityOption'; import { safeCallback } from 'utils'; @@ -29,37 +30,46 @@ export function ActionsMenu({ text={intl.get('view_details')} onClick={safeCallback(onViewDetails, original)} /> - - } - text={intl.get('edit_customer')} - onClick={safeCallback(onEdit, original)} - /> - } - text={intl.get('duplicate')} - onClick={safeCallback(onDuplicate, original)} - /> - + + + } - onClick={safeCallback(onInactivate, original)} + icon={} + text={intl.get('edit_customer')} + onClick={safeCallback(onEdit, original)} /> - - + + } - onClick={safeCallback(onActivate, original)} + icon={} + text={intl.get('duplicate')} + onClick={safeCallback(onDuplicate, original)} /> - - } - text={intl.get('delete_customer')} - intent={Intent.DANGER} - onClick={safeCallback(onDelete, original)} - /> + + + + } + onClick={safeCallback(onInactivate, original)} + /> + + + } + onClick={safeCallback(onActivate, original)} + /> + + + + } + text={intl.get('delete_customer')} + intent={Intent.DANGER} + onClick={safeCallback(onDelete, original)} + /> + ); } diff --git a/src/containers/Customers/CustomersUniversalSearch.js b/src/containers/Customers/CustomersUniversalSearch.js index b9e4b8768..1828678dc 100644 --- a/src/containers/Customers/CustomersUniversalSearch.js +++ b/src/containers/Customers/CustomersUniversalSearch.js @@ -1,4 +1,5 @@ import intl from 'react-intl-universal'; +import { AbilitySubject, CustomerAction } from '../../common/abilityOption'; import { RESOURCES_TYPES } from '../../common/resourcesTypes'; import withDrawerActions from '../Drawer/withDrawerActions'; @@ -42,4 +43,8 @@ export const universalSearchCustomerBind = () => ({ optionItemLabel: intl.get('customers'), selectItemAction: CustomerUniversalSearchSelectAction, itemSelect: customersToSearch, + permission: { + ability: CustomerAction.View, + subject: AbilitySubject.Customer, + }, }); diff --git a/src/containers/Dialogs/InviteUserDialog/InviteUserDialog.schema.js b/src/containers/Dialogs/InviteUserDialog/InviteUserDialog.schema.js index ee0c34702..057f75e25 100644 --- a/src/containers/Dialogs/InviteUserDialog/InviteUserDialog.schema.js +++ b/src/containers/Dialogs/InviteUserDialog/InviteUserDialog.schema.js @@ -2,10 +2,8 @@ import * as Yup from 'yup'; import intl from 'react-intl-universal'; const Schema = Yup.object().shape({ - email: Yup.string() - .email() - .required() - .label(intl.get('email')), + email: Yup.string().email().required().label(intl.get('email')), + role_id: Yup.string().required().label(intl.get('roles.label.role_name_')), }); -export const InviteUserFormSchema = Schema; \ No newline at end of file +export const InviteUserFormSchema = Schema; diff --git a/src/containers/Dialogs/InviteUserDialog/InviteUserFormContent.js b/src/containers/Dialogs/InviteUserDialog/InviteUserFormContent.js index bcc989cea..c66fe492f 100644 --- a/src/containers/Dialogs/InviteUserDialog/InviteUserFormContent.js +++ b/src/containers/Dialogs/InviteUserDialog/InviteUserFormContent.js @@ -1,7 +1,11 @@ import React from 'react'; import { FormGroup, InputGroup, Intent, Button } from '@blueprintjs/core'; import { FastField, Form, useFormikContext, ErrorMessage } from 'formik'; -import { FormattedMessage as T } from 'components'; +import { + ListSelect, + FieldRequiredHint, + FormattedMessage as T, +} from 'components'; import { CLASSES } from 'common/classes'; import classNames from 'classnames'; import { inputIntent } from 'utils'; @@ -15,23 +19,24 @@ function InviteUserFormContent({ closeDialog, }) { const { isSubmitting } = useFormikContext(); - const { isEditMode, dialogName } = useInviteUserFormContext(); + const { isEditMode, dialogName, roles } = useInviteUserFormContext(); const handleClose = () => { closeDialog(dialogName); }; - + console.log(roles, 'XX'); return (

- + {/* ----------- Email ----------- */} {({ field, meta: { error, touched } }) => ( } + labelInfo={} className={classNames('form-group--email', CLASSES.FILL)} intent={inputIntent({ error, touched })} helperText={} @@ -40,6 +45,31 @@ function InviteUserFormContent({ )} + {/* ----------- Role name ----------- */} + + {({ form, field: { value }, meta: { error, touched } }) => ( + } + labelInfo={} + helperText={} + className={classNames(CLASSES.FILL, 'form-group--role_name')} + intent={inputIntent({ error, touched })} + > + { + form.setFieldValue('role_id', id); + }} + selectedItem={value} + selectedItemProp={'id'} + textProp={'name'} + // labelProp={'id '} + popoverProps={{ minimal: true }} + intent={inputIntent({ error, touched })} + /> + + )} +
diff --git a/src/containers/Dialogs/InviteUserDialog/InviteUserFormProvider.js b/src/containers/Dialogs/InviteUserDialog/InviteUserFormProvider.js index aa093c209..dcaaad4cb 100644 --- a/src/containers/Dialogs/InviteUserDialog/InviteUserFormProvider.js +++ b/src/containers/Dialogs/InviteUserDialog/InviteUserFormProvider.js @@ -1,5 +1,5 @@ import React, { createContext } from 'react'; -import { useCreateInviteUser, useUsers } from 'hooks/query'; +import { useCreateInviteUser, useUsers, useRoles } from 'hooks/query'; import { DialogContent } from 'components'; const InviteUserFormContext = createContext(); @@ -14,6 +14,9 @@ function InviteUserFormProvider({ userId, isEditMode, dialogName, ...props }) { // fetch users list. const { isLoading: isUsersLoading } = useUsers(); + // fetch roles list. + const { data: roles, isLoading: isRolesLoading } = useRoles(); + // Provider state. const provider = { inviteUserMutate, @@ -21,10 +24,14 @@ function InviteUserFormProvider({ userId, isEditMode, dialogName, ...props }) { userId, isUsersLoading, isEditMode, + roles, }; return ( - + ); diff --git a/src/containers/Dialogs/UserFormDialog/UserForm.js b/src/containers/Dialogs/UserFormDialog/UserForm.js index 75a65251c..c9f18d410 100644 --- a/src/containers/Dialogs/UserFormDialog/UserForm.js +++ b/src/containers/Dialogs/UserFormDialog/UserForm.js @@ -10,6 +10,7 @@ import withDialogActions from 'containers/Dialog/withDialogActions'; import { UserFormSchema } from './UserForm.schema'; import UserFormContent from './UserFormContent'; import { useUserFormContext } from './UserFormProvider'; +import { transformErrors } from './utils'; import { compose, objectKeysTransform } from 'utils'; @@ -20,13 +21,10 @@ function UserForm({ // #withDialogActions closeDialog, }) { - const { - dialogName, - user, - userId, - isEditMode, - EditUserMutate, - } = useUserFormContext(); + const [calloutCode, setCalloutCode] = React.useState([]); + + const { dialogName, user, userId, isEditMode, EditUserMutate } = + useUserFormContext(); const initialValues = { ...(isEditMode && @@ -59,7 +57,7 @@ function UserForm({ data: { errors }, }, } = error; - + transformErrors(errors, { setErrors, setCalloutCode }); setSubmitting(false); }; @@ -72,7 +70,7 @@ function UserForm({ initialValues={initialValues} onSubmit={handleSubmit} > - + ); } diff --git a/src/containers/Dialogs/UserFormDialog/UserForm.schema.js b/src/containers/Dialogs/UserFormDialog/UserForm.schema.js index c7d245827..347319e3e 100644 --- a/src/containers/Dialogs/UserFormDialog/UserForm.schema.js +++ b/src/containers/Dialogs/UserFormDialog/UserForm.schema.js @@ -2,20 +2,14 @@ import * as Yup from 'yup'; import intl from 'react-intl-universal'; const Schema = Yup.object().shape({ - email: Yup.string() - .email() - .required() - .label(intl.get('email')), - first_name: Yup.string() - .required() - .label(intl.get('first_name_')), - last_name: Yup.string() - .required() - .label(intl.get('last_name_')), + email: Yup.string().email().required().label(intl.get('email')), + first_name: Yup.string().required().label(intl.get('first_name_')), + last_name: Yup.string().required().label(intl.get('last_name_')), phone_number: Yup.string() .matches() .required() .label(intl.get('phone_number_')), + role_id: Yup.string().required().label(intl.get('roles.label.role_name_')), }); export const UserFormSchema = Schema; diff --git a/src/containers/Dialogs/UserFormDialog/UserFormContent.js b/src/containers/Dialogs/UserFormDialog/UserFormContent.js index 46d168d97..f43787157 100644 --- a/src/containers/Dialogs/UserFormDialog/UserFormContent.js +++ b/src/containers/Dialogs/UserFormDialog/UserFormContent.js @@ -11,20 +11,22 @@ import { FormattedMessage as T } from 'components'; import { CLASSES } from 'common/classes'; import classNames from 'classnames'; import { inputIntent } from 'utils'; -import { FieldRequiredHint } from 'components'; +import { ListSelect, FieldRequiredHint } from 'components'; import { useUserFormContext } from './UserFormProvider'; import withDialogActions from 'containers/Dialog/withDialogActions'; import { compose } from 'utils'; +import { UserFormCalloutAlerts } from './components'; /** * User form content. */ function UserFormContent({ + calloutCode, // #withDialogActions closeDialog, }) { const { isSubmitting } = useFormikContext(); - const { dialogName } = useUserFormContext(); + const { dialogName, roles, isAuth } = useUserFormContext(); const handleClose = () => { closeDialog(dialogName); @@ -33,6 +35,8 @@ function UserFormContent({ return (
+ + {/* ----------- Email ----------- */} {({ field, meta: { error, touched } }) => ( @@ -88,6 +92,32 @@ function UserFormContent({ )} + {/* ----------- Role name ----------- */} + + {({ form, field: { value }, meta: { error, touched } }) => ( + } + labelInfo={} + helperText={} + className={classNames(CLASSES.FILL, 'form-group--role_name')} + intent={inputIntent({ error, touched })} + > + { + form.setFieldValue('role_id', id); + }} + selectedItem={value} + selectedItemProp={'id'} + textProp={'name'} + // labelProp={'id '} + popoverProps={{ minimal: true }} + intent={inputIntent({ error, touched })} + disabled={isAuth} + /> + + )} +
diff --git a/src/containers/Dialogs/UserFormDialog/UserFormProvider.js b/src/containers/Dialogs/UserFormDialog/UserFormProvider.js index 253b6f3bd..d4030e053 100644 --- a/src/containers/Dialogs/UserFormDialog/UserFormProvider.js +++ b/src/containers/Dialogs/UserFormDialog/UserFormProvider.js @@ -1,5 +1,10 @@ import React, { createContext, useContext } from 'react'; -import { useEditUser, useUser } from 'hooks/query'; +import { + useEditUser, + useUser, + useRoles, + useAuthenticatedAccount, +} from 'hooks/query'; import { DialogContent } from 'components'; @@ -17,10 +22,21 @@ function UserFormProvider({ userId, dialogName, ...props }) { enabled: !!userId, }); + // fetch roles list. + const { data: roles, isLoading: isRolesLoading } = useRoles(); + + // Retrieve authenticated user information. + const { + data: { id }, + } = useAuthenticatedAccount(); + const isEditMode = userId; + const isAuth = user.system_user_id == id + // Provider state. const provider = { + isAuth, userId, dialogName, @@ -28,10 +44,14 @@ function UserFormProvider({ userId, dialogName, ...props }) { EditUserMutate, isEditMode, + roles, }; return ( - + ); diff --git a/src/containers/Dialogs/UserFormDialog/components.js b/src/containers/Dialogs/UserFormDialog/components.js new file mode 100644 index 000000000..687cdd164 --- /dev/null +++ b/src/containers/Dialogs/UserFormDialog/components.js @@ -0,0 +1,14 @@ +import React from 'react'; +import intl from 'react-intl-universal'; +import { includes } from 'lodash'; +import { Callout, Intent } from '@blueprintjs/core'; + +export const UserFormCalloutAlerts = ({ calloutCodes }) => { + return [ + includes(calloutCodes, 200) && ( + + {intl.get('roles.error.you_cannot_change_your_own_role')} + + ), + ]; +}; diff --git a/src/containers/Dialogs/UserFormDialog/utils.js b/src/containers/Dialogs/UserFormDialog/utils.js new file mode 100644 index 000000000..8ec73acaa --- /dev/null +++ b/src/containers/Dialogs/UserFormDialog/utils.js @@ -0,0 +1,13 @@ +import intl from 'react-intl-universal'; + +// handle delete errors. +export const transformErrors = (errors, { setErrors, setCalloutCode }) => { + if ( + errors.find((error) => error.type === 'CANNOT_AUTHORIZED_USER_MUTATE_ROLE') + ) { + setCalloutCode([200]); + setErrors({ + role_id: intl.get('roles.error.you_cannot_change_your_own_role'), + }); + } +}; diff --git a/src/containers/Drawers/AccountDrawer/AccountDrawerActionBar.js b/src/containers/Drawers/AccountDrawer/AccountDrawerActionBar.js index cd4f11d90..a65f3042b 100644 --- a/src/containers/Drawers/AccountDrawer/AccountDrawerActionBar.js +++ b/src/containers/Drawers/AccountDrawer/AccountDrawerActionBar.js @@ -7,13 +7,14 @@ import { Intent, NavbarDivider, } from '@blueprintjs/core'; -import { FormattedMessage as T } from 'components'; +import { Can, FormattedMessage as T } from 'components'; import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar'; import withDialogActions from 'containers/Dialog/withDialogActions'; import withAlertsActions from 'containers/Alert/withAlertActions'; import { safeCallback } from 'utils'; +import { AccountAction, AbilitySubject } from '../../../common/abilityOption'; import { compose } from 'utils'; import { useAccountDrawerContext } from './AccountDrawerProvider'; @@ -53,26 +54,31 @@ function AccountDrawerActionBar({ return ( - + + - + + } /> diff --git a/src/containers/Expenses/ExpensesLanding/components.js b/src/containers/Expenses/ExpensesLanding/components.js index f288c1fc2..14f77d23b 100644 --- a/src/containers/Expenses/ExpensesLanding/components.js +++ b/src/containers/Expenses/ExpensesLanding/components.js @@ -12,13 +12,14 @@ import { MenuDivider, } from '@blueprintjs/core'; import intl from 'react-intl-universal'; - +import { ExpenseAction, AbilitySubject } from '../../../common/abilityOption'; import { FormatDateCell, FormattedMessage as T, Money, Icon, If, + Can, } from 'components'; import { safeCallback } from 'utils'; @@ -54,25 +55,31 @@ export function ActionsMenu({ text={intl.get('view_details')} onClick={safeCallback(onViewDetails, original)} /> - - + + + + } + text={intl.get('publish_expense')} + onClick={safeCallback(onPublish, original)} + /> + + + } - text={intl.get('publish_expense')} - onClick={safeCallback(onPublish, original)} + icon={} + text={intl.get('edit_expense')} + onClick={safeCallback(onEdit, original)} /> - - } - text={intl.get('edit_expense')} - onClick={safeCallback(onEdit, original)} - /> - } - text={intl.get('delete_expense')} - intent={Intent.DANGER} - onClick={safeCallback(onDelete, original)} - /> + + + } + text={intl.get('delete_expense')} + intent={Intent.DANGER} + onClick={safeCallback(onDelete, original)} + /> + ); } diff --git a/src/containers/FinancialStatements/FilterFinancialReports.js b/src/containers/FinancialStatements/FilterFinancialReports.js new file mode 100644 index 000000000..ea3e8c1a8 --- /dev/null +++ b/src/containers/FinancialStatements/FilterFinancialReports.js @@ -0,0 +1,23 @@ +import { isEmpty } from 'lodash'; +import { useAbilityContext } from '../../hooks'; + +function useFilterFinancialReports(financialSection) { + const ability = useAbilityContext(); + + const section = financialSection + .map((section) => { + const reports = section.reports.filter((report) => { + return ability.can(report.ability, report.subject); + }); + + return { + sectionTitle: section.sectionTitle, + reports, + }; + }) + .filter(({ reports }) => !isEmpty(reports)); + + return section; +} + +export default useFilterFinancialReports; diff --git a/src/containers/FinancialStatements/FinancialReports.js b/src/containers/FinancialStatements/FinancialReports.js index 19d576f51..5f6f770a6 100644 --- a/src/containers/FinancialStatements/FinancialReports.js +++ b/src/containers/FinancialStatements/FinancialReports.js @@ -1,7 +1,7 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { For } from 'components'; - +import useFilterFinancialReports from './FilterFinancialReports'; import DashboardInsider from 'components/Dashboard/DashboardInsider'; import { financialReportMenus, @@ -37,13 +37,18 @@ function FinancialReportsSection({ sectionTitle, reports }) { * Financial reports. */ export default function FinancialReports() { + const financialReportMenu = useFilterFinancialReports(financialReportMenus); + const SalesAndPurchasesReportMenu = useFilterFinancialReports( + SalesAndPurchasesReportMenus, + ); + return (
- +
diff --git a/src/containers/GlobalErrors/GlobalErrors.js b/src/containers/GlobalErrors/GlobalErrors.js index 55a45382c..20ae7dae9 100644 --- a/src/containers/GlobalErrors/GlobalErrors.js +++ b/src/containers/GlobalErrors/GlobalErrors.js @@ -1,7 +1,5 @@ -import React from 'react'; import { Intent } from '@blueprintjs/core'; import intl from 'react-intl-universal'; -import { useHistory } from 'react-router-dom'; import AppToaster from 'components/AppToaster'; import withGlobalErrors from './withGlobalErrors'; @@ -30,7 +28,6 @@ function GlobalErrors({ toastKeySessionExpired, ); } - if (globalErrors.session_expired) { toastKeySomethingWrong = AppToaster.show( { @@ -43,6 +40,18 @@ function GlobalErrors({ toastKeySomethingWrong, ); } + if (globalErrors.access_denied) { + toastKeySomethingWrong = AppToaster.show( + { + message: 'You do not have permissions to access this page.', + intent: Intent.DANGER, + onDismiss: () => { + globalErrorsSet({ access_denied: false }); + }, + }, + toastKeySomethingWrong, + ); + } return null; } diff --git a/src/containers/Homepage/ShortcutBoxesSection.js b/src/containers/Homepage/ShortcutBoxesSection.js index cf712e952..d9bcb90f0 100644 --- a/src/containers/Homepage/ShortcutBoxesSection.js +++ b/src/containers/Homepage/ShortcutBoxesSection.js @@ -3,6 +3,7 @@ import { Link } from 'react-router-dom'; import { For } from 'components'; import 'style/pages/FinancialStatements/FinancialSheets.scss'; +import { useFilterShortcutBoxesSection } from './components'; function ShortcutBox({ title, link, description }) { return ( @@ -27,5 +28,6 @@ function ShortcutBoxes({ sectionTitle, shortcuts }) { } export default function ShortcutBoxesSection({ section }) { - return ; + const BoxSection = useFilterShortcutBoxesSection(section); + return ; } diff --git a/src/containers/Homepage/components.js b/src/containers/Homepage/components.js new file mode 100644 index 000000000..b2701ef28 --- /dev/null +++ b/src/containers/Homepage/components.js @@ -0,0 +1,18 @@ +import { isEmpty } from 'lodash'; +import { useAbilityContext } from '../../hooks'; + +export const useFilterShortcutBoxesSection = (section) => { + const ability = useAbilityContext(); + + return section + .map(({ sectionTitle, shortcuts }) => { + const shortcut = shortcuts.filter((shortcuts) => { + return ability.can(shortcuts.ability, shortcuts.subject); + }); + return { + sectionTitle: sectionTitle, + shortcuts: shortcut, + }; + }) + .filter(({ shortcuts }) => !isEmpty(shortcuts)); +}; diff --git a/src/containers/InventoryAdjustments/components.js b/src/containers/InventoryAdjustments/components.js index eaa923e08..49e74316f 100644 --- a/src/containers/InventoryAdjustments/components.js +++ b/src/containers/InventoryAdjustments/components.js @@ -12,10 +12,14 @@ import { import intl from 'react-intl-universal'; import moment from 'moment'; -import { FormattedMessage as T } from 'components'; +import { FormattedMessage as T, Can } from 'components'; import { isNumber } from 'lodash'; import { Icon, Money, If } from 'components'; import { isBlank, safeCallback } from 'utils'; +import { + InventoryAdjustmentAction, + AbilitySubject, +} from '../../common/abilityOption'; /** * Publish accessor @@ -102,20 +106,31 @@ export const ActionsMenu = ({ text={intl.get('view_details')} onClick={safeCallback(onViewDetails, original)} /> - - + + + + + } + text={intl.get('publish_adjustment')} + onClick={safeCallback(onPublish, original)} + /> + + + } - text={intl.get('publish_adjustment')} - onClick={safeCallback(onPublish, original)} + text={intl.get('delete_adjustment')} + intent={Intent.DANGER} + onClick={safeCallback(onDelete, original)} + icon={} /> - - } - /> + ); }; diff --git a/src/containers/Items/ItemsActionsBar.js b/src/containers/Items/ItemsActionsBar.js index e882ac19d..6468ab7df 100644 --- a/src/containers/Items/ItemsActionsBar.js +++ b/src/containers/Items/ItemsActionsBar.js @@ -14,6 +14,7 @@ import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar'; import Icon from 'components/Icon'; import { If, + Can, DashboardActionViewsList, AdvancedFilterPopover, DashboardFilterButton, @@ -30,8 +31,7 @@ import withSettings from '../Settings/withSettings'; import { compose } from 'utils'; import withSettingsActions from '../Settings/withSettingsActions'; - -import { Can, AbilityContext } from '../../components/Abilities'; +import { ItemAction, AbilitySubject } from '../../common/abilityOption'; /** * Items actions bar. @@ -60,8 +60,6 @@ function ItemsActionsBar({ // Items refresh action. const { refresh } = useRefreshItems(); - const { ability } = React.useContext(AbilityContext); - // History context. const history = useHistory(); @@ -106,14 +104,14 @@ function ItemsActionsBar({ /> - {/* */} + + + - + + } /> diff --git a/src/containers/Items/ItemsUniversalSearch.js b/src/containers/Items/ItemsUniversalSearch.js index a261ed3fd..b63ced91b 100644 --- a/src/containers/Items/ItemsUniversalSearch.js +++ b/src/containers/Items/ItemsUniversalSearch.js @@ -1,7 +1,10 @@ import intl from 'react-intl-universal'; -import { RESOURCES_TYPES } from '../../common/resourcesTypes'; + import withDrawerActions from '../Drawer/withDrawerActions'; +import { RESOURCES_TYPES } from '../../common/resourcesTypes'; +import { AbilitySubject, ItemAction } from '../../common/abilityOption'; + /** * Item univrsal search item select action. */ @@ -46,4 +49,8 @@ export const universalSearchItemBind = () => ({ optionItemLabel: intl.get('items'), selectItemAction: ItemUniversalSearchSelectAction, itemSelect: transfromItemsToSearch, + permission: { + ability: ItemAction.View, + subject: AbilitySubject.Item, + }, }); diff --git a/src/containers/Items/components.js b/src/containers/Items/components.js index d573f4aaf..bbc01fb84 100644 --- a/src/containers/Items/components.js +++ b/src/containers/Items/components.js @@ -12,8 +12,13 @@ import { import intl from 'react-intl-universal'; import { isNumber } from 'lodash'; -import { FormattedMessage as T, Icon, Money, If } from 'components'; +import { FormattedMessage as T, Icon, Money, If, Can } from 'components'; import { isBlank, safeCallback } from 'utils'; +import { + AbilitySubject, + ItemAction, + InventoryAdjustmentAction, +} from '../../common/abilityOption'; /** * Publish accessor @@ -90,44 +95,58 @@ export function ItemsActionMenuList({ text={} onClick={safeCallback(onViewDetails, original)} /> - - } - text={intl.get('edit_item')} - onClick={safeCallback(onEditItem, original)} - /> - } - text={intl.get('duplicate')} - onClick={safeCallback(onDuplicate, original)} - /> - + + } - onClick={safeCallback(onInactivateItem, original)} + icon={} + text={intl.get('edit_item')} + onClick={safeCallback(onEditItem, original)} /> - - + + } - onClick={safeCallback(onActivateItem, original)} + icon={} + text={intl.get('duplicate')} + onClick={safeCallback(onDuplicate, original)} /> - - + + + + } + onClick={safeCallback(onInactivateItem, original)} + /> + + + + } + onClick={safeCallback(onActivateItem, original)} + /> + + + + + } + onClick={safeCallback(onMakeAdjustment, original)} + /> + + + } - onClick={safeCallback(onMakeAdjustment, original)} + text={intl.get('delete_item')} + icon={} + onClick={safeCallback(onDeleteItem, original)} + intent={Intent.DANGER} /> - - } - onClick={safeCallback(onDeleteItem, original)} - intent={Intent.DANGER} - /> + ); } diff --git a/src/containers/KeyboardShortcuts/ShortcutsTable.js b/src/containers/KeyboardShortcuts/ShortcutsTable.js index da5974556..2d70b6d85 100644 --- a/src/containers/KeyboardShortcuts/ShortcutsTable.js +++ b/src/containers/KeyboardShortcuts/ShortcutsTable.js @@ -1,14 +1,13 @@ import React, { useMemo } from 'react'; import { DataTable } from 'components'; -import { FormattedMessage as T } from 'components'; import intl from 'react-intl-universal'; -import keyboardShortcuts from 'common/keyboardShortcutsOptions'; +import { useKeywordShortcuts } from '../../hooks/dashboard'; /** * keyboard shortcuts table. */ -function ShortcutsTable() { - +export default function ShortcutsTable() { + const keywordShortcuts = useKeywordShortcuts(); const columns = useMemo( () => [ @@ -30,8 +29,5 @@ function ShortcutsTable() { ], [], ); - - return ; + return ; } - -export default ShortcutsTable; diff --git a/src/containers/Preferences/Currencies/components.js b/src/containers/Preferences/Currencies/components.js index 51eb2371c..431628b27 100644 --- a/src/containers/Preferences/Currencies/components.js +++ b/src/containers/Preferences/Currencies/components.js @@ -67,7 +67,7 @@ export function useCurrenciesTableColumns() { width: 120, }, { - Header: 'Currency sign', + Header: intl.get('currency_sign'), width: 120, accessor: 'currency_sign' }, diff --git a/src/containers/Preferences/Users/Roles/RolesForm/RolesForm.js b/src/containers/Preferences/Users/Roles/RolesForm/RolesForm.js index 37f352b2b..a1af99e4d 100644 --- a/src/containers/Preferences/Users/Roles/RolesForm/RolesForm.js +++ b/src/containers/Preferences/Users/Roles/RolesForm/RolesForm.js @@ -1,7 +1,8 @@ import React from 'react'; +import { useHistory } from 'react-router-dom'; import intl from 'react-intl-universal'; import { Formik } from 'formik'; -import { defaultTo, sumBy, isEmpty } from 'lodash'; +import { isEmpty } from 'lodash'; import 'style/pages/Preferences/Roles/Form.scss'; @@ -12,7 +13,11 @@ import { AppToaster, FormattedMessage as T } from 'components'; import { CreateRolesFormSchema, EditRolesFormSchema } from './RolesForm.schema'; import { useRolesFormContext } from './RolesFormProvider'; -import { transformToArray } from './utils'; +import { + getNewRoleInitialValues, + transformToArray, + transformToObject, +} from './utils'; import RolesFormContent from './RolesFormContent'; import withDashboardActions from 'containers/Dashboard/withDashboardActions'; @@ -32,24 +37,31 @@ function RolesForm({ // #withDashboardActions changePreferencesPageTitle, }) { + // History context. + const history = useHistory(); + + // Role form context. const { isNewMode, createRolePermissionMutate, editRolePermissionMutate, - permissionSchema, + permissionsSchema, + role, roleId, } = useRolesFormContext(); // Initial values. const initialValues = { ...defaultValues, - ...transformToForm(permissionSchema, defaultValues), + ...(!isEmpty(role) + ? transformToForm(transformToObject(role), defaultValues) + : getNewRoleInitialValues(permissionsSchema)), }; - React.useEffect(() => { changePreferencesPageTitle(); }, [changePreferencesPageTitle]); + // Handle the form submit. const handleFormSubmit = (values, { setSubmitting }) => { const permission = transformToArray(values); const form = { @@ -67,6 +79,7 @@ function RolesForm({ intent: Intent.SUCCESS, }); setSubmitting(false); + history.push('/preferences/users'); }; const onError = (errors) => { diff --git a/src/containers/Preferences/Users/Roles/RolesForm/RolesForm.schema.js b/src/containers/Preferences/Users/Roles/RolesForm/RolesForm.schema.js index ab1012778..b2f40ecdb 100644 --- a/src/containers/Preferences/Users/Roles/RolesForm/RolesForm.schema.js +++ b/src/containers/Preferences/Users/Roles/RolesForm/RolesForm.schema.js @@ -3,7 +3,7 @@ import intl from 'react-intl-universal'; import { DATATYPES_LENGTH } from 'common/dataTypes'; const Schema = Yup.object().shape({ - role_name: Yup.string().required().label(intl.get('roles.label.role_name')), + role_name: Yup.string().required().label(intl.get('roles.label.role_name_')), role_description: Yup.string().nullable().max(DATATYPES_LENGTH.TEXT), permissions: Yup.object().shape({ diff --git a/src/containers/Preferences/Users/Roles/RolesForm/RolesFormPage.js b/src/containers/Preferences/Users/Roles/RolesForm/RolesFormPage.js index a307d9195..14accbe68 100644 --- a/src/containers/Preferences/Users/Roles/RolesForm/RolesFormPage.js +++ b/src/containers/Preferences/Users/Roles/RolesForm/RolesFormPage.js @@ -1,4 +1,5 @@ import React from 'react'; +import { useParams } from 'react-router-dom'; import RolesForm from './RolesForm'; import { RolesFormProvider } from './RolesFormProvider'; @@ -6,8 +7,11 @@ import { RolesFormProvider } from './RolesFormProvider'; * Roles Form page. */ export default function RolesFormPage() { + const { id } = useParams(); + const idInteger = parseInt(id, 10); + return ( - + ); diff --git a/src/containers/Preferences/Users/Roles/RolesForm/RolesFormProvider.js b/src/containers/Preferences/Users/Roles/RolesForm/RolesFormProvider.js index 3e8cb2982..06fb0d076 100644 --- a/src/containers/Preferences/Users/Roles/RolesForm/RolesFormProvider.js +++ b/src/containers/Preferences/Users/Roles/RolesForm/RolesFormProvider.js @@ -25,29 +25,27 @@ function RolesFormProvider({ roleId, ...props }) { const { mutateAsync: editRolePermissionMutate } = useEditRolePermissionSchema(); + // Retrieve permissions schema. const { data: permissionsSchema, isLoading: isPermissionsSchemaLoading, isFetching: isPermissionsSchemaFetching, } = usePermissionsSchema(); - // const roleId = 6; - - const { data: permission, isLoading: isPermissionLoading } = + const { data: role, isLoading: isPermissionLoading } = useRolePermission(roleId, { enabled: !!roleId, }); + // Detarmines whether the new or edit mode. const isNewMode = !roleId; - const permissionSchema = transformToObject(permission); - // Provider state. const provider = { isNewMode, roleId, + role, permissionsSchema, - permissionSchema, isPermissionsSchemaLoading, isPermissionsSchemaFetching, createRolePermissionMutate, diff --git a/src/containers/Preferences/Users/Roles/RolesForm/utils.js b/src/containers/Preferences/Users/Roles/RolesForm/utils.js index bdb6ef36a..3d73bb7bf 100644 --- a/src/containers/Preferences/Users/Roles/RolesForm/utils.js +++ b/src/containers/Preferences/Users/Roles/RolesForm/utils.js @@ -1,5 +1,3 @@ -import { isEmpty } from 'lodash'; - export const transformToArray = ({ permissions }) => { return Object.keys(permissions).map((index) => { const [value, key] = index.split('/'); @@ -12,16 +10,44 @@ export const transformToArray = ({ permissions }) => { }); }; -export const transformToObject = ({ name, description, permissions }) => { - if (!isEmpty(permissions)) { - const output = {}; - permissions.forEach((item) => { - output[`${item.subject}/${item.ability}`] = !!item.value; - }); - return { - role_name: name, - role_description: description, - permissions: { ...output }, - }; - } +export const transformPermissionsToObject = (permissions) => { + const output = {}; + permissions.forEach((item) => { + output[`${item.subject}/${item.ability}`] = !!item.value; + }); + return output; +}; + +export const transformToObject = (role) => { + return { + role_name: role.name, + role_description: role.description, + permissions: transformPermissionsToObject(role.permissions), + }; +}; + +export const getDefaultValuesFromSchema = (schema) => { + return schema + .map((item) => { + const abilities = [ + ...(item.abilities || []), + ...(item.extra_abilities || []), + ]; + return abilities + .filter((ability) => ability.default) + .map((ability) => ({ + subject: item.subject, + ability: ability.key, + value: ability.default, + })); + }) + .flat(); +}; + +export const getNewRoleInitialValues = (schema) => { + return { + permissions: transformPermissionsToObject( + getDefaultValuesFromSchema(schema), + ), + }; }; diff --git a/src/containers/Preferences/Users/Roles/RolesLanding/RolesDataTable.js b/src/containers/Preferences/Users/Roles/RolesLanding/RolesDataTable.js index bfc5ee67a..8337ffde7 100644 --- a/src/containers/Preferences/Users/Roles/RolesLanding/RolesDataTable.js +++ b/src/containers/Preferences/Users/Roles/RolesLanding/RolesDataTable.js @@ -1,11 +1,15 @@ import React from 'react'; +import { Intent } from '@blueprintjs/core'; +import { useHistory } from 'react-router-dom'; import intl from 'react-intl-universal'; +import styled from 'styled-components'; -import { DataTable } from 'components'; +import { DataTable, AppToaster } from 'components'; import TableSkeletonRows from 'components/Datatable/TableSkeletonRows'; import { useRolesTableColumns, ActionsMenu } from './components'; import withAlertsActions from 'containers/Alert/withAlertActions'; +import { useRolesContext } from './RolesListProvider'; import { compose } from 'utils'; @@ -16,26 +20,59 @@ function RolesDataTable({ // #withAlertsActions openAlert, }) { + // History context. + const history = useHistory(); + + // Retrieve roles table columns const columns = useRolesTableColumns(); - const handleDeleteRole = ({ id }) => { - openAlert('role-delete', { roleId: id }); + // Roles table context. + const { roles, isRolesFetching, isRolesLoading } = useRolesContext(); + + // handles delete the given role. + const handleDeleteRole = ({ id, predefined }) => { + if (predefined) { + AppToaster.show({ + message: intl.get('roles.error.you_cannot_delete_predefined_roles'), + intent: Intent.DANGER, + }); + } else { + openAlert('role-delete', { roleId: id }); + } + }; + // Handles the edit of the given role. + const handleEditRole = ({ id, predefined }) => { + if (predefined) { + AppToaster.show({ + message: intl.get('roles.error.you_cannot_edit_predefined_roles'), + intent: Intent.DANGER, + }); + } else { + history.push(`/preferences/roles/${id}`); + } }; - // const Data = [{ name: 'AH', description: 'Description' }]; return ( - ); } +const RolesTable = styled(DataTable)` + .table .tr { + min-height: 42px; + } +`; + export default compose(withAlertsActions)(RolesDataTable); diff --git a/src/containers/Preferences/Users/Roles/RolesLanding/RolesList.js b/src/containers/Preferences/Users/Roles/RolesLanding/RolesList.js index 436adb6e5..095b905d2 100644 --- a/src/containers/Preferences/Users/Roles/RolesLanding/RolesList.js +++ b/src/containers/Preferences/Users/Roles/RolesLanding/RolesList.js @@ -1,5 +1,4 @@ import React from 'react'; -import intl from 'react-intl-universal'; import { RolesListProvider } from './RolesListProvider'; import RolesDataTable from './RolesDataTable'; diff --git a/src/containers/Preferences/Users/Roles/RolesLanding/RolesListProvider.js b/src/containers/Preferences/Users/Roles/RolesLanding/RolesListProvider.js index 248b47260..2303754fb 100644 --- a/src/containers/Preferences/Users/Roles/RolesLanding/RolesListProvider.js +++ b/src/containers/Preferences/Users/Roles/RolesLanding/RolesListProvider.js @@ -1,7 +1,7 @@ import React from 'react'; import classNames from 'classnames'; import { CLASSES } from 'common/classes'; -// import {} from 'hooks/query'; +import { useRoles } from 'hooks/query'; const RolesListContext = React.createContext(); @@ -9,8 +9,19 @@ const RolesListContext = React.createContext(); * Roles list provider. */ function RolesListProvider({ ...props }) { + // Fetch roles list. + const { + data: roles, + isFetching: isRolesFetching, + isLoading: isRolesLoading, + } = useRoles(); + // Provider state. - const provider = {}; + const provider = { + roles, + isRolesFetching, + isRolesLoading, + }; return (
} text={intl.get('roles.edit_roles')} + onClick={safeCallback(onEditRole, original)} /> { + if (errors.find((error) => error.type === 'ROLE_PREFINED')) { + AppToaster.show({ + message: intl.get('roles.error.role_is_predefined'), + intent: Intent.DANGER, + }); + } +}; diff --git a/src/containers/Preferences/Users/components.js b/src/containers/Preferences/Users/components.js index 853852a61..dcdaa0389 100644 --- a/src/containers/Preferences/Users/components.js +++ b/src/containers/Preferences/Users/components.js @@ -28,8 +28,6 @@ export function ActionsMenu({ row: { original }, payload: { onEdit, onInactivate, onActivate, onDelete, onResendInvitation }, }) { - - return ( @@ -78,7 +76,7 @@ export function ActionsMenu({ */ function StatusAccessor(user) { return !user.is_invite_accepted ? ( - + ) : user.active ? ( @@ -111,8 +109,6 @@ function FullNameAccessor(user) { } export const useUsersListColumns = () => { - - return React.useMemo( () => [ { @@ -134,14 +130,20 @@ export const useUsersListColumns = () => { width: 150, }, { - id: 'phone_number', - Header: intl.get('phone_number'), - accessor: 'phone_number', + id: 'role_name', + Header: intl.get('users.column.role_name'), + accessor: 'role.name', width: 120, }, + // { + // id: 'phone_number', + // Header: intl.get('phone_number'), + // accessor: 'phone_number', + // width: 120, + // }, { id: 'status', - Header: 'Status', + Header: intl.get('status'), accessor: StatusAccessor, width: 80, className: 'status', diff --git a/src/containers/Purchases/Bills/BillUniversalSearch.js b/src/containers/Purchases/Bills/BillUniversalSearch.js index 5d80bd6d0..e4426f846 100644 --- a/src/containers/Purchases/Bills/BillUniversalSearch.js +++ b/src/containers/Purchases/Bills/BillUniversalSearch.js @@ -7,6 +7,7 @@ import { T, Icon, Choose, If } from 'components'; import { RESOURCES_TYPES } from 'common/resourcesTypes'; import withDrawerActions from '../../Drawer/withDrawerActions'; +import { AbilitySubject, BillAction } from '../../../common/abilityOption'; /** * Universal search bill item select action. @@ -116,4 +117,8 @@ export const universalSearchBillBind = () => ({ selectItemAction: BillUniversalSearchSelect, itemRenderer: BillUniversalSearchItem, itemSelect: billsToSearch, + permission: { + ability: BillAction.View, + subject: AbilitySubject.Bill, + }, }); diff --git a/src/containers/Purchases/Bills/BillsLanding/BillsActionsBar.js b/src/containers/Purchases/Bills/BillsLanding/BillsActionsBar.js index d021d63b2..fe4dba317 100644 --- a/src/containers/Purchases/Bills/BillsLanding/BillsActionsBar.js +++ b/src/containers/Purchases/Bills/BillsLanding/BillsActionsBar.js @@ -14,12 +14,14 @@ import { useHistory } from 'react-router-dom'; import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar'; import { If, + Can, FormattedMessage as T, DashboardActionViewsList, DashboardFilterButton, AdvancedFilterPopover, DashboardRowsHeightButton, } from 'components'; +import { BillAction, AbilitySubject } from '../../../../common/abilityOption'; import withBillsActions from './withBillsActions'; import withBills from './withBills'; @@ -86,12 +88,14 @@ function BillActionsBar({ onChange={handleTabChange} /> - + + - + + } /> diff --git a/src/containers/Purchases/Bills/BillsLanding/components.js b/src/containers/Purchases/Bills/BillsLanding/components.js index 9cd1ec6fa..61bcd403f 100644 --- a/src/containers/Purchases/Bills/BillsLanding/components.js +++ b/src/containers/Purchases/Bills/BillsLanding/components.js @@ -16,8 +16,14 @@ import { If, Choose, Money, + Can, } from 'components'; import { formattedAmount, safeCallback, isBlank, calculateStatus } from 'utils'; +import { + BillAction, + PaymentMadeAction, + AbilitySubject, +} from '../../../../common/abilityOption'; /** * Actions menu. @@ -40,38 +46,44 @@ export function ActionsMenu({ text={intl.get('view_details')} onClick={safeCallback(onViewDetails, original)} /> - - } - text={intl.get('edit_bill')} - onClick={safeCallback(onEdit, original)} - /> + + + } + text={intl.get('edit_bill')} + onClick={safeCallback(onEdit, original)} + /> - - } - text={intl.get('mark_as_opened')} - onClick={safeCallback(onOpen, original)} - /> - - - } - text={intl.get('add_payment')} - onClick={safeCallback(onQuick, original)} - /> - + + } + text={intl.get('mark_as_opened')} + onClick={safeCallback(onOpen, original)} + /> + + + + + } + text={intl.get('add_payment')} + onClick={safeCallback(onQuick, original)} + /> + + } text={intl.get('allocate_landed_coast')} onClick={safeCallback(onAllocateLandedCost, original)} /> - } - /> + + } + /> + ); } diff --git a/src/containers/Purchases/PaymentMades/PaymentMadeUniversalSearch.js b/src/containers/Purchases/PaymentMades/PaymentMadeUniversalSearch.js index bd423d01b..d6c4e513d 100644 --- a/src/containers/Purchases/PaymentMades/PaymentMadeUniversalSearch.js +++ b/src/containers/Purchases/PaymentMades/PaymentMadeUniversalSearch.js @@ -1,14 +1,14 @@ import React from 'react'; import { MenuItem } from '@blueprintjs/core'; import intl from 'react-intl-universal'; -import { isEmpty } from 'lodash'; -import { Icon, If } from 'components'; +import { Icon } from 'components'; import { RESOURCES_TYPES } from 'common/resourcesTypes'; import withDrawerActions from '../../Drawer/withDrawerActions'; import { highlightText } from 'utils'; +import { AbilitySubject, PaymentMadeAction } from '../../../common/abilityOption'; /** * Universal search bill item select action. @@ -82,4 +82,8 @@ export const universalSearchPaymentMadeBind = () => ({ selectItemAction: PaymentMadeUniversalSearchSelect, itemRenderer: PaymentMadeUniversalSearchItem, itemSelect: paymentMadeToSearch, + permission: { + ability: PaymentMadeAction.View, + subject: AbilitySubject.PaymentMade, + }, }); diff --git a/src/containers/Purchases/PaymentMades/PaymentsLanding/PaymentMadeActionsBar.js b/src/containers/Purchases/PaymentMades/PaymentsLanding/PaymentMadeActionsBar.js index b3c8776c0..770bddc48 100644 --- a/src/containers/Purchases/PaymentMades/PaymentsLanding/PaymentMadeActionsBar.js +++ b/src/containers/Purchases/PaymentMades/PaymentsLanding/PaymentMadeActionsBar.js @@ -14,6 +14,7 @@ import { useHistory } from 'react-router-dom'; import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar'; import { If, + Can, FormattedMessage as T, DashboardActionViewsList, DashboardFilterButton, @@ -29,6 +30,10 @@ import withSettings from 'containers/Settings/withSettings'; import { usePaymentMadesListContext } from './PaymentMadesListProvider'; import { useRefreshPaymentMades } from 'hooks/query/paymentMades'; +import { + PaymentMadeAction, + AbilitySubject, +} from '../../../../common/abilityOption'; import { compose } from 'utils'; @@ -70,7 +75,7 @@ function PaymentMadeActionsBar({ const handleRefreshBtnClick = () => { refresh(); }; - + // Handle table row size change. const handleTableRowSizeChange = (size) => { addSetting('billPayments', 'tableSize', size); @@ -85,12 +90,14 @@ function PaymentMadeActionsBar({ onChange={handleTabChange} /> - + + - + + } /> diff --git a/src/containers/Purchases/PaymentMades/PaymentsLanding/components.js b/src/containers/Purchases/PaymentMades/PaymentsLanding/components.js index 875817ca7..ae5951478 100644 --- a/src/containers/Purchases/PaymentMades/PaymentsLanding/components.js +++ b/src/containers/Purchases/PaymentMades/PaymentsLanding/components.js @@ -10,7 +10,12 @@ import { } from '@blueprintjs/core'; import intl from 'react-intl-universal'; -import { Icon, Money, FormatDateCell } from 'components'; +import { Icon, Money, FormatDateCell, Can } from 'components'; +import { + PaymentMadeAction, + AbilitySubject, +} from '../../../../common/abilityOption'; + import { safeCallback } from 'utils'; export function AmountAccessor(row) { @@ -31,18 +36,23 @@ export function ActionsMenu({ text={intl.get('view_details')} onClick={safeCallback(onViewDetails, original)} /> - - } - text={intl.get('edit_payment_made')} - onClick={safeCallback(onEdit, original)} - /> - } - /> + + + + } + text={intl.get('edit_payment_made')} + onClick={safeCallback(onEdit, original)} + /> + + + } + /> + ); } diff --git a/src/containers/QuickNewDropdown/QuickNewDropdown.js b/src/containers/QuickNewDropdown/QuickNewDropdown.js index a3489d21d..a6242e2af 100644 --- a/src/containers/QuickNewDropdown/QuickNewDropdown.js +++ b/src/containers/QuickNewDropdown/QuickNewDropdown.js @@ -4,16 +4,21 @@ import { FormattedMessage as T } from 'components'; import { useHistory } from 'react-router-dom'; import { Icon } from 'components'; import { Position } from '@blueprintjs/core'; -import { getQuickNewActions } from 'common/quickNewOptions'; import { Select } from '@blueprintjs/select'; +import { useGetQuickNewMenu } from 'common/quickNewOptions'; + /** * Quick New Dropdown. */ export default function QuickNewDropdown() { const history = useHistory(); - const quickNewOptions = getQuickNewActions(); + const quickNewOptions = useGetQuickNewMenu(); + // Can't continue if there is no any quick new menu items to display. + if (quickNewOptions.length === 0) { + return null; + } // Handle click quick new button. const handleClickQuickNew = ({ path }) => { history.push(`/${path}`); @@ -40,4 +45,4 @@ export default function QuickNewDropdown() { /> ); -} \ No newline at end of file +} diff --git a/src/containers/Sales/Estimates/EstimatesLanding/EstimateUniversalSearch.js b/src/containers/Sales/Estimates/EstimatesLanding/EstimateUniversalSearch.js index 3d979a4c4..a3c68f542 100644 --- a/src/containers/Sales/Estimates/EstimatesLanding/EstimateUniversalSearch.js +++ b/src/containers/Sales/Estimates/EstimatesLanding/EstimateUniversalSearch.js @@ -5,6 +5,8 @@ import intl from 'react-intl-universal'; import { Choose, T, Icon } from 'components'; import { RESOURCES_TYPES } from "common/resourcesTypes"; +import { AbilitySubject, SaleEstimateAction } from '../../../../common/abilityOption'; + import withDrawerActions from "../../../Drawer/withDrawerActions"; /** @@ -110,5 +112,9 @@ export const universalSearchEstimateBind = () => ({ optionItemLabel: intl.get('estimates'), selectItemAction: EstimateUniversalSearchSelect, itemRenderer: EstimateUniversalSearchItem, - itemSelect: transformEstimatesToSearch + itemSelect: transformEstimatesToSearch, + permission: { + ability: SaleEstimateAction.View, + subject: AbilitySubject.Estimate, + }, }); diff --git a/src/containers/Sales/Estimates/EstimatesLanding/EstimatesActionsBar.js b/src/containers/Sales/Estimates/EstimatesLanding/EstimatesActionsBar.js index e25b09083..46d37c963 100644 --- a/src/containers/Sales/Estimates/EstimatesLanding/EstimatesActionsBar.js +++ b/src/containers/Sales/Estimates/EstimatesLanding/EstimatesActionsBar.js @@ -11,6 +11,7 @@ import { import { useHistory } from 'react-router-dom'; import { + Can, FormattedMessage as T, AdvancedFilterPopover, If, @@ -28,6 +29,10 @@ import withSettings from 'containers/Settings/withSettings'; import { useEstimatesListContext } from './EstimatesListProvider'; import { useRefreshEstimates } from 'hooks/query/estimates'; +import { + SaleEstimateAction, + AbilitySubject, +} from '../../../../common/abilityOption'; import { compose } from 'utils'; @@ -87,12 +92,14 @@ function EstimateActionsBar({ onChange={handleTabChange} /> - - + + + + } /> diff --git a/src/containers/Sales/Estimates/EstimatesLanding/components.js b/src/containers/Sales/Estimates/EstimatesLanding/components.js index 22c95f3ae..dd9d72131 100644 --- a/src/containers/Sales/Estimates/EstimatesLanding/components.js +++ b/src/containers/Sales/Estimates/EstimatesLanding/components.js @@ -2,6 +2,10 @@ import React from 'react'; import { Intent, Tag, Menu, MenuItem, MenuDivider } from '@blueprintjs/core'; import intl from 'react-intl-universal'; import clsx from 'classnames'; +import { + SaleEstimateAction, + AbilitySubject, +} from '../../../../common/abilityOption'; import { CLASSES } from '../../../../common/classes'; import { @@ -11,6 +15,7 @@ import { Choose, Icon, If, + Can, } from 'components'; import { safeCallback } from 'utils'; @@ -68,63 +73,74 @@ export function ActionsMenu({ text={intl.get('view_details')} onClick={safeCallback(onViewDetails, original)} /> - - } - text={intl.get('edit_estimate')} - onClick={safeCallback(onEdit, original)} - /> - } - text={intl.get('convert_to_invoice')} - onClick={safeCallback(onConvert, original)} - /> - + + } - text={intl.get('mark_as_delivered')} - onClick={safeCallback(onDeliver, original)} + icon={} + text={intl.get('edit_estimate')} + onClick={safeCallback(onEdit, original)} /> - - - - } - text={intl.get('mark_as_rejected')} - onClick={safeCallback(onReject, original)} - /> - - + } + text={intl.get('convert_to_invoice')} + onClick={safeCallback(onConvert, original)} + /> + + } - text={intl.get('mark_as_approved')} - onClick={safeCallback(onApprove, original)} + text={intl.get('mark_as_delivered')} + onClick={safeCallback(onDeliver, original)} /> - - - } - text={intl.get('mark_as_approved')} - onClick={safeCallback(onApprove, original)} - /> - } - text={intl.get('mark_as_rejected')} - onClick={safeCallback(onReject, original)} - /> - - - } - text={intl.get('print')} - onClick={safeCallback(onPrint, original)} - /> - } - /> + + + + } + text={intl.get('mark_as_rejected')} + onClick={safeCallback(onReject, original)} + /> + + + } + text={intl.get('mark_as_approved')} + onClick={safeCallback(onApprove, original)} + /> + + + } + text={intl.get('mark_as_approved')} + onClick={safeCallback(onApprove, original)} + /> + } + text={intl.get('mark_as_rejected')} + onClick={safeCallback(onReject, original)} + /> + + + + + } + text={intl.get('print')} + onClick={safeCallback(onPrint, original)} + /> + + + } + /> + ); } diff --git a/src/containers/Sales/Invoices/InvoiceUniversalSearch.js b/src/containers/Sales/Invoices/InvoiceUniversalSearch.js index db287a724..6725234a4 100644 --- a/src/containers/Sales/Invoices/InvoiceUniversalSearch.js +++ b/src/containers/Sales/Invoices/InvoiceUniversalSearch.js @@ -3,10 +3,13 @@ import intl from 'react-intl-universal'; import { MenuItem } from '@blueprintjs/core'; import { T, Choose, Icon } from 'components'; - import { highlightText } from 'utils'; import { RESOURCES_TYPES } from 'common/resourcesTypes'; +import { + AbilitySubject, + SaleInvoiceAction, +} from '../../../common/abilityOption'; import withDrawerActions from '../../Drawer/withDrawerActions'; /** @@ -118,4 +121,8 @@ export const universalSearchInvoiceBind = () => ({ selectItemAction: InvoiceUniversalSearchSelect, itemRenderer: InvoiceUniversalSearchItem, itemSelect: transformInvoicesToSearch, + permission: { + ability: SaleInvoiceAction.View, + subject: AbilitySubject.Invoice, + }, }); diff --git a/src/containers/Sales/Invoices/InvoicesLanding/InvoicesActionsBar.js b/src/containers/Sales/Invoices/InvoicesLanding/InvoicesActionsBar.js index 6b03ab3a7..a99227f48 100644 --- a/src/containers/Sales/Invoices/InvoicesLanding/InvoicesActionsBar.js +++ b/src/containers/Sales/Invoices/InvoicesLanding/InvoicesActionsBar.js @@ -18,7 +18,11 @@ import { import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar'; -import { If, DashboardActionViewsList } from 'components'; +import { Can, If, DashboardActionViewsList } from 'components'; +import { + SaleInvoiceAction, + AbilitySubject, +} from '../../../../common/abilityOption'; import { useRefreshInvoices } from 'hooks/query/invoices'; import { useInvoicesListContext } from './InvoicesListProvider'; @@ -84,12 +88,14 @@ function InvoiceActionsBar({ onChange={handleTabChange} /> - - - + + + + } /> diff --git a/src/containers/Sales/Invoices/InvoicesLanding/components.js b/src/containers/Sales/Invoices/InvoicesLanding/components.js index 2efde999e..68e68ae89 100644 --- a/src/containers/Sales/Invoices/InvoicesLanding/components.js +++ b/src/containers/Sales/Invoices/InvoicesLanding/components.js @@ -18,8 +18,14 @@ import { Choose, If, Icon, + Can, } from 'components'; import { formattedAmount, safeCallback, calculateStatus } from 'utils'; +import { + SaleInvoiceAction, + PaymentReceiveAction, + AbilitySubject, +} from '../../../../common/abilityOption'; export const statusAccessor = (row) => { return ( @@ -55,7 +61,6 @@ export const statusAccessor = (row) => { })} - - } - text={intl.get('edit_invoice')} - onClick={safeCallback(onEdit, original)} - /> - + + } - text={intl.get('mark_as_delivered')} - onClick={safeCallback(onDeliver, original)} + icon={} + text={intl.get('edit_invoice')} + onClick={safeCallback(onEdit, original)} /> - - + + + } + text={intl.get('mark_as_delivered')} + onClick={safeCallback(onDeliver, original)} + /> + + + + + } + text={intl.get('add_payment')} + onClick={safeCallback(onQuick, original)} + /> + + + } - text={intl.get('add_payment')} - onClick={safeCallback(onQuick, original)} + icon={} + text={intl.get('print')} + onClick={safeCallback(onPrint, original)} /> - - } - text={intl.get('print')} - onClick={safeCallback(onPrint, original)} - /> - } - /> + + + } + /> + ); } diff --git a/src/containers/Sales/PaymentReceives/PaymentReceiveUniversalSearch.js b/src/containers/Sales/PaymentReceives/PaymentReceiveUniversalSearch.js index bff1faa22..163a4811c 100644 --- a/src/containers/Sales/PaymentReceives/PaymentReceiveUniversalSearch.js +++ b/src/containers/Sales/PaymentReceives/PaymentReceiveUniversalSearch.js @@ -2,13 +2,17 @@ import React from 'react'; import { MenuItem } from '@blueprintjs/core'; import intl from 'react-intl-universal'; -import { RESOURCES_TYPES } from "../../../common/resourcesTypes"; -import withDrawerActions from "../../Drawer/withDrawerActions"; +import { RESOURCES_TYPES } from '../../../common/resourcesTypes'; +import withDrawerActions from '../../Drawer/withDrawerActions'; import { highlightText } from 'utils'; import { Icon } from 'components'; +import { + AbilitySubject, + PaymentReceiveAction, +} from '../../../common/abilityOption'; /** - * Payment receive universal search item select action. + * Payment receive universal search item select action. */ function PaymentReceiveUniversalSearchSelectComponent({ // #ownProps @@ -19,7 +23,9 @@ function PaymentReceiveUniversalSearchSelectComponent({ openDrawer, }) { if (resourceType === RESOURCES_TYPES.PAYMENT_RECEIVE) { - openDrawer('payment-receive-detail-drawer', { paymentReceiveId: resourceId }); + openDrawer('payment-receive-detail-drawer', { + paymentReceiveId: resourceId, + }); } return null; } @@ -59,7 +65,7 @@ export function PaymentReceiveUniversalSearchItem( /** * Transformes payment receives to search. */ - const paymentReceivesToSearch = (payment) => ({ +const paymentReceivesToSearch = (payment) => ({ id: payment.id, text: payment.customer.display_name, subText: payment.formatted_payment_date, @@ -70,10 +76,14 @@ export function PaymentReceiveUniversalSearchItem( /** * Binds universal search payment receive configure. */ - export const universalSearchPaymentReceiveBind = () => ({ +export const universalSearchPaymentReceiveBind = () => ({ resourceType: RESOURCES_TYPES.PAYMENT_RECEIVE, optionItemLabel: intl.get('payment_receives'), selectItemAction: PaymentReceiveUniversalSearchSelect, itemRenderer: PaymentReceiveUniversalSearchItem, itemSelect: paymentReceivesToSearch, + permission: { + ability: PaymentReceiveAction.View, + subject: AbilitySubject.PaymentReceive, + }, }); diff --git a/src/containers/Sales/PaymentReceives/PaymentsLanding/PaymentReceiveActionsBar.js b/src/containers/Sales/PaymentReceives/PaymentsLanding/PaymentReceiveActionsBar.js index bb67f91a3..8e9a196f3 100644 --- a/src/containers/Sales/PaymentReceives/PaymentsLanding/PaymentReceiveActionsBar.js +++ b/src/containers/Sales/PaymentReceives/PaymentsLanding/PaymentReceiveActionsBar.js @@ -19,14 +19,17 @@ import { import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar'; -import { If, DashboardActionViewsList } from 'components'; +import { Can, If, DashboardActionViewsList } from 'components'; import withPaymentReceivesActions from './withPaymentReceivesActions'; import withPaymentReceives from './withPaymentReceives'; import withSettingsActions from 'containers/Settings/withSettingsActions'; import withSettings from 'containers/Settings/withSettings'; - +import { + PaymentReceiveAction, + AbilitySubject, +} from '../../../../common/abilityOption'; import { compose } from 'utils'; import { usePaymentReceivesListContext } from './PaymentReceiptsListProvider'; import { useRefreshPaymentReceive } from 'hooks/query/paymentReceives'; @@ -85,12 +88,14 @@ function PaymentReceiveActionsBar({ onChange={handleTabChange} /> - + - + + } /> diff --git a/src/containers/Sales/PaymentReceives/PaymentsLanding/components.js b/src/containers/Sales/PaymentReceives/PaymentsLanding/components.js index aa5d7c8cf..de95d270a 100644 --- a/src/containers/Sales/PaymentReceives/PaymentsLanding/components.js +++ b/src/containers/Sales/PaymentReceives/PaymentsLanding/components.js @@ -11,10 +11,13 @@ import { import intl from 'react-intl-universal'; import clsx from 'classnames'; -import { FormatDateCell, Money, Icon } from 'components'; +import { FormatDateCell, Money, Icon, Can } from 'components'; import { safeCallback } from 'utils'; import { CLASSES } from '../../../../common/classes'; - +import { + PaymentReceiveAction, + AbilitySubject, +} from '../../../../common/abilityOption'; /** * Table actions menu. */ @@ -29,18 +32,22 @@ export function ActionsMenu({ text={intl.get('view_details')} onClick={safeCallback(onViewDetails, paymentReceive)} /> - - } - text={intl.get('edit_payment_receive')} - onClick={safeCallback(onEdit, paymentReceive)} - /> - } - /> + + + } + text={intl.get('edit_payment_receive')} + onClick={safeCallback(onEdit, paymentReceive)} + /> + + + } + /> + ); } diff --git a/src/containers/Sales/Receipts/ReceiptUniversalSearch.js b/src/containers/Sales/Receipts/ReceiptUniversalSearch.js index b2156d8da..8db5a4d87 100644 --- a/src/containers/Sales/Receipts/ReceiptUniversalSearch.js +++ b/src/containers/Sales/Receipts/ReceiptUniversalSearch.js @@ -1,16 +1,18 @@ - import React from 'react'; import intl from 'react-intl-universal'; import { MenuItem } from '@blueprintjs/core'; import { Icon, Choose, T } from 'components'; -import { RESOURCES_TYPES } from "../../../common/resourcesTypes"; -import withDrawerActions from "../../Drawer/withDrawerActions"; - +import { RESOURCES_TYPES } from '../../../common/resourcesTypes'; +import withDrawerActions from '../../Drawer/withDrawerActions'; +import { + AbilitySubject, + SaleReceiptAction, +} from '../../../common/abilityOption'; /** - * Receipt universal search item select action. + * Receipt universal search item select action. */ function ReceiptUniversalSearchSelectComponent({ // #ownProps @@ -39,11 +41,15 @@ function ReceiptStatus({ receipt }) { return ( - + + + - + + + ); @@ -100,4 +106,8 @@ export const universalSearchReceiptBind = () => ({ selectItemAction: ReceiptUniversalSearchSelect, itemRenderer: ReceiptUniversalSearchItem, itemSelect: transformReceiptsToSearch, + permission: { + ability: SaleReceiptAction.View, + subject: AbilitySubject.Receipt, + }, }); diff --git a/src/containers/Sales/Receipts/ReceiptsLanding/ReceiptActionsBar.js b/src/containers/Sales/Receipts/ReceiptsLanding/ReceiptActionsBar.js index 4f7020d2d..fe9a5eed5 100644 --- a/src/containers/Sales/Receipts/ReceiptsLanding/ReceiptActionsBar.js +++ b/src/containers/Sales/Receipts/ReceiptsLanding/ReceiptActionsBar.js @@ -17,7 +17,7 @@ import { DashboardRowsHeightButton, } from 'components'; -import { If, DashboardActionViewsList } from 'components'; +import { Can, If, DashboardActionViewsList } from 'components'; import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar'; import withReceiptsActions from './withReceiptsActions'; @@ -28,6 +28,11 @@ import withSettings from 'containers/Settings/withSettings'; import { useReceiptsListContext } from './ReceiptsListProvider'; import { useRefreshReceipts } from 'hooks/query/receipts'; +import { + SaleReceiptAction, + AbilitySubject, +} from '../../../../common/abilityOption'; + import { compose } from 'utils'; /** @@ -87,12 +92,14 @@ function ReceiptActionsBar({ /> - + + - + + } /> diff --git a/src/containers/Sales/Receipts/ReceiptsLanding/components.js b/src/containers/Sales/Receipts/ReceiptsLanding/components.js index 7f7f0cbda..8a063e69d 100644 --- a/src/containers/Sales/Receipts/ReceiptsLanding/components.js +++ b/src/containers/Sales/Receipts/ReceiptsLanding/components.js @@ -15,7 +15,11 @@ import clsx from 'classnames'; import { CLASSES } from '../../../../common/classes'; import { safeCallback } from 'utils'; -import { FormatDateCell, Choose, Money, Icon, If } from 'components'; +import { FormatDateCell, Choose, Money, Icon, If, Can } from 'components'; +import { + SaleReceiptAction, + AbilitySubject, +} from '../../../../common/abilityOption'; export function ActionsMenu({ payload: { onEdit, onDelete, onClose, onDrawer, onViewDetails, onPrint }, @@ -28,30 +32,37 @@ export function ActionsMenu({ text={intl.get('view_details')} onClick={safeCallback(onViewDetails, receipt)} /> - - } - text={intl.get('edit_receipt')} - onClick={safeCallback(onEdit, receipt)} - /> - + + } - text={intl.get('mark_as_closed')} - onClick={safeCallback(onClose, receipt)} + icon={} + text={intl.get('edit_receipt')} + onClick={safeCallback(onEdit, receipt)} /> - - } - text={intl.get('print')} - onClick={safeCallback(onPrint, receipt)} - /> - } - /> + + + } + text={intl.get('mark_as_closed')} + onClick={safeCallback(onClose, receipt)} + /> + + + + } + text={intl.get('print')} + onClick={safeCallback(onPrint, receipt)} + /> + + + } + /> + ); } diff --git a/src/containers/UniversalSearch/DashboardUniversalSearch.js b/src/containers/UniversalSearch/DashboardUniversalSearch.js index 2accd802b..415c75c50 100644 --- a/src/containers/UniversalSearch/DashboardUniversalSearch.js +++ b/src/containers/UniversalSearch/DashboardUniversalSearch.js @@ -14,7 +14,7 @@ import DashboardUniversalSearchItemActions from './DashboardUniversalSearchItemA import { DashboardUniversalSearchItem } from './components'; import DashboardUniversalSearchHotkeys from './DashboardUniversalSearchHotkeys'; -import { getUniversalSearchTypeOptions } from './utils'; +import { useGetUniversalSearchTypeOptions } from './utils'; /** * Dashboard universal search. @@ -28,6 +28,8 @@ function DashboardUniversalSearch({ closeGlobalSearch, defaultUniversalResourceType, }) { + const searchTypeOptions = useGetUniversalSearchTypeOptions(); + // Search keyword. const [searchKeyword, setSearchKeyword] = React.useState(''); @@ -97,10 +99,9 @@ function DashboardUniversalSearch({ setSearchKeyword(''); }; - const searchTypeOptions = React.useMemo( - () => getUniversalSearchTypeOptions(), - [], - ); + if (searchTypeOptions.length === 0) { + return null; + } return (