fix issues.

This commit is contained in:
Ahmed Bouhuolia
2020-05-05 04:21:37 +02:00
parent 3b25056cbe
commit bd7eb0eb76
41 changed files with 364 additions and 216 deletions

View File

@@ -14,7 +14,7 @@ import {
} from '@blueprintjs/core';
import classNames from 'classnames';
import { connect } from 'react-redux';
import { useRouteMatch } from 'react-router-dom';
import { useRouteMatch, useHistory } from 'react-router-dom';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import DialogConnect from 'connectors/Dialog.connector';
import AccountsConnect from 'connectors/Accounts.connector';
@@ -32,14 +32,21 @@ function AccountsActionsBar({
onBulkDelete,
onBulkArchive,
}) {
const {path} = useRouteMatch();
const history = useHistory();
const onClickNewAccount = () => { openDialog('account-form', {}); };
const accountsFields = getResourceFields('accounts');
const [filterCount, setFilterCount] = useState(0);
const onClickViewItem = (view) => {
history.push(view
? `/dashboard/accounts/${view.id}/custom_view` :
'/dashboard/accounts');
};
const viewsMenuItems = views.map((view) => {
return (<MenuItem href={`${path}/${view.id}/custom_view`} text={view.name} />);
return (<MenuItem onClick={() => onClickViewItem(view)} text={view.name} />);
});
const hasSelectedRows = useMemo(() => selectedRows.length > 0, [selectedRows]);
@@ -131,15 +138,8 @@ function AccountsActionsBar({
);
}
const mapStateToProps = state => {
return {
// selectedRows: state.accounts.selectedRows
};
};
export default compose(
DialogConnect,
AccountsConnect,
ResourceConnect,
connect(mapStateToProps),
)(AccountsActionsBar);

View File

@@ -21,7 +21,9 @@ import DataTable from 'components/DataTable';
import Money from 'components/Money';
import { useUpdateEffect } from 'hooks';
function AccountsDataTable({
loading,
accounts,
onDeleteAccount,
onInactiveAccount,
@@ -34,7 +36,6 @@ function AccountsDataTable({
onFetchData,
onSelectedRowsChange
}) {
const {custom_view_id: customViewId} = useParams();
const [initialMount, setInitialMount] = useState(false);
useUpdateEffect(() => {
@@ -43,19 +44,6 @@ function AccountsDataTable({
}
}, [accountsLoading, setInitialMount]);
useEffect(() => {
const viewMeta = getViewItem(customViewId);
if (customViewId) {
changeCurrentView(customViewId);
setTopbarEditView(customViewId);
}
changePageSubtitle((customViewId && viewMeta) ? viewMeta.name : '');
}, [customViewId]);
// Clear page subtitle when unmount the page.
useEffect(() => () => { changePageSubtitle(''); }, []);
const handleEditAccount = useCallback((account) => () => {
openDialog('account-form', { action: 'edit', id: account.id });
}, [openDialog]);
@@ -173,17 +161,19 @@ function AccountsDataTable({
}, [onSelectedRowsChange]);
return (
<DataTable
columns={columns}
data={accounts}
onFetchData={handleDatatableFetchData}
manualSortBy={true}
selectionColumn={selectionColumn}
expandable={true}
treeGraph={true}
onSelectedRowsChange={handleSelectedRowsChange}
loading={accountsLoading && !initialMount}
spinnerProps={{size: 30}} />
<LoadingIndicator loading={loading} mount={false}>
<DataTable
columns={columns}
data={accounts}
onFetchData={handleDatatableFetchData}
manualSortBy={true}
selectionColumn={selectionColumn}
expandable={true}
treeGraph={true}
onSelectedRowsChange={handleSelectedRowsChange}
loading={accountsLoading && !initialMount}
spinnerProps={{size: 30}} />
</LoadingIndicator>
);
}

View File

@@ -1,4 +1,4 @@
import React, {useEffect} from 'react';
import React, {useEffect, useCallback} from 'react';
import { useHistory } from 'react-router';
import { connect } from 'react-redux';
import {
@@ -16,6 +16,7 @@ import { compose } from 'utils';
import AccountsConnect from 'connectors/Accounts.connector';
import DashboardConnect from 'connectors/Dashboard.connector';
import {useUpdateEffect} from 'hooks';
import ViewConnect from 'connectors/View.connector';
function AccountsViewsTabs({
views,
@@ -23,59 +24,74 @@ function AccountsViewsTabs({
customViewChanged,
addAccountsTableQueries,
onViewChanged,
getViewItem,
changeCurrentView,
changePageSubtitle,
}) {
const history = useHistory();
const { custom_view_id: customViewId } = useParams();
const { custom_view_id: customViewId = null } = useParams();
useEffect(() => {
const viewMeta = getViewItem(customViewId);
changeCurrentView(customViewId || -1);
setTopbarEditView(customViewId);
changePageSubtitle((customViewId && viewMeta) ? viewMeta.name : '');
}, [customViewId]);
// Clear page subtitle when unmount the page.
useEffect(() => () => { changePageSubtitle(''); }, []);
// Handle click a new view tab.
const handleClickNewView = () => {
setTopbarEditView(null);
history.push('/dashboard/custom_views/accounts/new');
};
// Handle view tab link click.
const handleViewLinkClick = () => {
setTopbarEditView(customViewId);
}
};
useUpdateEffect(() => {
useEffect(() => {
customViewChanged && customViewChanged(customViewId);
addAccountsTableQueries({
custom_view_id: customViewId || null,
custom_view_id: customViewId,
});
}, [customViewId]);
useUpdateEffect(() => {
onViewChanged && onViewChanged(customViewId);
}, [customViewId]);
useEffect(() => {
addAccountsTableQueries({
custom_view_id: customViewId,
})
}, [customViewId]);
const tabs = views.map(view => {
const tabs = views.map((view) => {
const baseUrl = '/dashboard/accounts';
const link = (
<Link
to={`${baseUrl}/${view.id}/custom_view`}
onClick={handleViewLinkClick}
>{view.name}</Link>
>{ view.name }</Link>
);
return <Tab
id={`custom_view_${view.id}`}
title={link} />;
return <Tab id={`custom_view_${view.id}`} title={link} />;
});
return (
<Navbar className='navbar--dashboard-views'>
<NavbarGroup align={Alignment.LEFT}>
<Tabs
id='navbar'
large={true}
selectedTabId={`custom_view_${customViewId}`}
selectedTabId={customViewId ? `custom_view_${customViewId}` : 'all'}
className='tabs--dashboard-views'
>
<Tab
id='all'
title={<Link to={`/dashboard/accounts`}>All</Link>} />
{tabs}
id={'all'}
title={<Link to={`/dashboard/accounts`}>All</Link>}
onClick={handleViewLinkClick}
/>
{ tabs }
<Button
className='button--new-view'
icon={<Icon icon='plus' />}
@@ -91,4 +107,5 @@ function AccountsViewsTabs({
export default compose(
AccountsConnect,
DashboardConnect,
ViewConnect,
)(AccountsViewsTabs);

View File

@@ -7,7 +7,7 @@ import PreferencesContent from 'components/Preferences/PreferencesContent';
import PreferencesSidebar from 'components/Preferences/PreferencesSidebar';
import Search from 'containers/Dashboard/GeneralSearch/Search';
export default function () {
export default function Dashboard() {
return (
<div className='dashboard'>
<Switch>

View File

@@ -9,8 +9,8 @@ export default function DashboardContentRoute() {
<Switch>
{ routes.map((route, index) => (
<Route
exact
// key={index}
exact={route.exact}
key={index}
path={`${route.path}`}
component={route.component} />
))}

View File

@@ -1,10 +1,14 @@
import React from 'react';
import { Route, Switch, useRouteMatch } from 'react-router-dom';
import { Route, Switch, Redirect } from 'react-router-dom';
import preferencesRoutes from 'routes/preferences'
export default function DashboardContentRoute() {
const defaultTab = '/dashboard/preferences/general';
return (
<Route pathname="/dashboard/preferences">
<Redirect from='/dashboard/preferences' to={defaultTab} />
<Switch>
{ preferencesRoutes.map((route, index) => (
<Route

View File

@@ -1,11 +1,31 @@
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import DashboardTopbarUser from 'components/Dashboard/TopbarUser';
import UsersActions from 'containers/Dashboard/Preferences/UsersActions';
import CurrenciesActions from 'containers/Dashboard/Preferences/CurrenciesActions';
export default function PreferencesTopbar() {
return (
<div class="dashboard__preferences-topbar">
<h2>Accounts</h2>
<div class="preferences__topbar-actions">
<Route pathname="/dashboard/preferences">
<Switch>
<Route
exact
path={'/dashboard/preferences/users'}
component={UsersActions} />
<Route
exact
path={'/dashboard/preferences/currencies'}
component={CurrenciesActions} />
</Switch>
</Route>
</div>
<div class="dashboard__topbar-user">
<DashboardTopbarUser />
</div>

View File

@@ -23,34 +23,4 @@ export default [
disabled: false,
href: '/dashboard/preferences/accounts',
},
{
text: 'Credit Notes',
disabled: false,
href: '/dashboard/preferences/credit_note',
},
{
text: 'Debit Notes',
disabled: false,
href: '/dashboard/preferences/debit_note',
},
{
text: 'Accountant',
disabled: false,
href: '/dashboard/preferences/accountant',
},
{
text: 'Accounts',
disabled: false,
href: '/dashboard/preferences/accounts',
},
{
text: 'Credit Notes',
disabled: false,
href: '/dashboard/preferences/credit_note',
},
{
text: 'Debit Notes',
disabled: false,
href: '/dashboard/preferences/debit_note',
},
];

View File

@@ -10,7 +10,7 @@ export const mapStateToProps = (state, props) => ({
export const mapDispatchToProps = (dispatch) => ({
requestSubmitMedia: (form, config) => dispatch(submitMedia({ form, config })),
requestDeleteMedia: (id) => dispatch(deleteMedia({ id })),
requestDeleteMedia: (ids) => dispatch(deleteMedia({ ids })),
});
export default connect(mapStateToProps, mapDispatchToProps);

View File

@@ -12,7 +12,7 @@ import {
Checkbox,
Position,
} from '@blueprintjs/core';
import AuthenticationToaster from 'components/AppToaster';
import Toaster from 'components/AppToaster';
import ErrorMessage from 'components/ErrorMessage';
import AuthInsider from 'containers/Authentication/AuthInsider';
import Icon from 'components/Icon';
@@ -69,7 +69,6 @@ function Login({
message: `The email and password you entered did not match our records.
Please double-check and try again.`,
intent: Intent.DANGER,
position: Position.BOTTOM,
});
}
if (errors.find((e) => e.type === ERRORS_TYPES.USER_INACTIVE)) {
@@ -79,7 +78,7 @@ function Login({
});
}
toastBuilders.forEach(builder => {
AuthenticationToaster.show(builder);
Toaster.show(builder);
});
setSubmitting(false);
});
@@ -114,7 +113,6 @@ function Login({
<InputGroup
intent={(errors.crediential && touched.crediential) && Intent.DANGER}
large={true}
placeholder={'name@company.com'}
{...getFieldProps('crediential')}
/>
</FormGroup>
@@ -130,7 +128,6 @@ function Login({
large={true}
intent={(errors.password && touched.password) && Intent.DANGER}
type={shown ? 'text' : 'password'}
placeholder={'password'}
{...getFieldProps('password')}
/>
</FormGroup>

View File

@@ -32,6 +32,8 @@ function AccountsChart({
const [bulkDelete, setBulkDelete] = useState(false);
const [selectedRows, setSelectedRows] = useState([]);
const [tableLoading, setTableLoading] = useState(false);
// Fetch accounts resource views and fields.
const fetchHook = useAsync(async () => {
await Promise.all([
@@ -66,7 +68,8 @@ function AccountsChart({
setDeleteAccount(false);
if (errors.find((e) => e.type === 'ACCOUNT.PREDEFINED')) {
AppToaster.show({
message: 'cannot_delete_predefined_account'
message: 'cannot_delete_predefined_account',
intent: Intent.DANGER,
});
}
if (errors.find((e) => e.type === 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS')) {
@@ -96,7 +99,7 @@ function AccountsChart({
});
}, [inactiveAccount, requestFetchAccountsTable, requestInactiveAccount]);
const handleEditAccount = (account) => {
};
@@ -138,7 +141,11 @@ function AccountsChart({
// Refetch accounts data table when current custom view changed.
const handleViewChanged = useCallback(() => {
fetchAccountsHook.execute();
setTableLoading(true);
fetchAccountsHook.execute().finally(() => {
setTableLoading(false);
});
}, [fetchAccountsHook]);
// Handle fetch data of accounts datatable.
@@ -177,7 +184,8 @@ function AccountsChart({
onRestoreAccount={handleRestoreAccount}
onEditAccount={handleEditAccount}
onFetchData={handleFetchData}
onSelectedRowsChange={handleSelectedRowsChange} />
onSelectedRowsChange={handleSelectedRowsChange}
loading={tableLoading} />
</Route>
</Switch>

View File

@@ -7,7 +7,8 @@ import {
Intent,
TextArea,
MenuItem,
Checkbox
Checkbox,
Position
} from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import * as Yup from 'yup';
@@ -92,7 +93,8 @@ function AccountFormDialog({
}).then((response) => {
closeDialog(name);
AppToaster.show({
message: 'the_account_has_been_edited'
message: 'the_account_has_been_edited',
intent: Intent.SUCCESS,
});
setSubmitting(false);
}).catch((errors) => {
@@ -103,7 +105,9 @@ function AccountFormDialog({
requestSubmitAccount({ form: { ...omit(values, exclude) } }).then(response => {
closeDialog(name);
AppToaster.show({
message: 'the_account_has_been_submit'
message: 'the_account_has_been_submit',
intent: Intent.SUCCESS,
position: Position.BOTTOM,
});
setSubmitting(false);
}).catch((errors) => {

View File

@@ -1,4 +1,4 @@
import React, { useMemo } from 'react';
import React, { useMemo, useCallback } from 'react';
import { useIntl } from 'react-intl';
import { useFormik } from 'formik';
import * as Yup from 'yup';
@@ -90,9 +90,9 @@ function InviteUserDialog({
fetchHook.execute();
};
const onDialogClosed = () => {
const onDialogClosed = useCallback(() => {
formik.resetForm();
};
}, [formik.resetForm]);
const handleClose = () => {
closeDialog(name);

View File

@@ -1,4 +1,4 @@
import React, { useMemo } from 'react';
import React, { useMemo, useCallback } from 'react';
import { useIntl } from 'react-intl';
import { useFormik } from 'formik';
import * as Yup from 'yup';
@@ -49,7 +49,14 @@ function UserFormDialog({
)),
};
const formik = useFormik({
const {
values,
errors,
touched,
resetForm,
getFieldProps,
handleSubmit,
} = useFormik({
enableReinitialize: true,
initialValues,
validationSchema,
@@ -74,15 +81,13 @@ function UserFormDialog({
}
},
});
const { values, errors, touched } = useMemo(() => formik, [formik]);
const onDialogOpening = () => {
fetchHook.execute();
};
const onDialogClosed = () => {
formik.resetForm();
};
const onDialogClosed = useCallback(() => {
resetForm();
}, [resetForm]);
const handleClose = () => {
closeDialog(name);
@@ -103,19 +108,21 @@ function UserFormDialog({
onClosed={onDialogClosed}
onOpening={onDialogOpening}
>
<form onSubmit={formik.handleSubmit}>
<form onSubmit={handleSubmit}>
<div className={Classes.DIALOG_BODY}>
<p class="mb2">Your teammate will get an email that gives them access to your team.</p>
<FormGroup
label={'Email'}
className={'form-group--email'}
intent={errors.email && touched.email && Intent.DANGER}
helperText={<ErrorMessage name='email' {...formik} />}
className={classNames('form-group--email', Classes.FILL)}
intent={(errors.email && touched.email) && Intent.DANGER}
helperText={<ErrorMessage name='email' {...{errors, touched}} />}
inline={true}
>
<InputGroup
medium={true}
intent={errors.email && touched.email && Intent.DANGER}
{...formik.getFieldProps('email')}
intent={(errors.email && touched.email) && Intent.DANGER}
{...getFieldProps('email')}
/>
</FormGroup>
</div>

View File

@@ -79,6 +79,20 @@ const ItemsActionsBar = ({
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon='plus' />}
text='New Item'
onClick={onClickNewItem}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon='plus' />}
text='New Category'
onClick={onClickNewCategory}
/>
<Popover
content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK}
@@ -91,19 +105,6 @@ const ItemsActionsBar = ({
/>
</Popover>
<Button
className={Classes.MINIMAL}
icon={<Icon icon='plus' />}
text='New Item'
onClick={onClickNewItem}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon='plus' />}
text='New Category'
onClick={onClickNewCategory}
/>
{hasSelectedRows && (
<Button
className={Classes.MINIMAL}

View File

@@ -63,7 +63,7 @@ const ItemCategoriesList = ({
}, [setSelectedRows]);
return (
<DashboardInsider loading={fetchHook.pending}>
<DashboardInsider loading={fetchHook.pending} name="items-categories">
<ItemsCategoryActionsBar
views={views}
onDeleteCategory={handelDeleteCategory}

View File

@@ -41,7 +41,7 @@ function ItemsList({
await Promise.all([
fetchResourceViews('items'),
fetchResourceFields('items'),
])
]);
});
const fetchItems = useAsync(async () => {

View File

@@ -11,9 +11,7 @@ function Currencies({ openDialog }) {
return (
<div className={'preferences__inside-content'}>
<div className={'preferences__tabs'}>
<Button intent={Intent.PRIMARY} onClick={onClickNewCurrency}>
New Currency
</Button>
</div>
</div>
);

View File

@@ -0,0 +1,31 @@
import React, {useCallback} from 'react';
import {
Button,
Intent,
} from '@blueprintjs/core';
import Icon from 'components/Icon';
import DialogConnect from 'connectors/Dialog.connector';
import {compose} from 'utils';
function CurrenciesActions({
openDialog,
}) {
const handleClickNewCurrency = useCallback(() => {
openDialog('currency-form');
}, []);
return (
<div class="users-actions">
<Button
icon={<Icon icon='plus' iconSize={12} />}
onClick={handleClickNewCurrency}
intent={Intent.PRIMARY}>
New Currency
</Button>
</div>
);
}
export default compose(
DialogConnect,
)(CurrenciesActions);

View File

@@ -19,8 +19,6 @@ import DialogConnect from 'connectors/Dialog.connector';
import DashboardConnect from 'connectors/Dashboard.connector';
import LoadingIndicator from 'components/LoadingIndicator';
import DataTable from 'components/DataTable';
import Currencies from './Currencies';
import useAsync from 'hooks/async';
import AppToaster from 'components/AppToaster';
function CurrenciesList({
@@ -109,11 +107,9 @@ function CurrenciesList({
const handleDatatableFetchData = useCallback(() => {
onFetchData && onFetchData();
}, []);
console.log({ currencies }, 'X');
return (
<LoadingIndicator>
<Currencies />
<DataTable
columns={columns}
data={Object.values(currencies)}

View File

@@ -6,26 +6,12 @@ import connector from 'connectors/UsersPreferences.connector';
function UsersPreferences({ openDialog }) {
const onChangeTabs = (currentTabId) => {};
const onClickNewUser = useCallback(() => {
openDialog('user-form');
}, [openDialog]);
return (
<div class='preferences__inside-content preferences__inside-content--users-roles'>
<div class='preferences__tabs'>
<Tabs animate={true} large={true} onChange={onChangeTabs}>
<Tabs animate={true} onChange={onChangeTabs}>
<Tab id='users' title='Users' />
<Tab id='roles' title='Roles' />
<div class='preferences__tabs-extra-actions'>
<Button intent={Intent.PRIMARY} onClick={onClickNewUser}>
Invite User
</Button>
<Button intent={Intent.PRIMARY} onClick={onClickNewUser}>
New Role
</Button>
</div>
</Tabs>
</div>
<PreferencesSubContent preferenceTab='users' />

View File

@@ -0,0 +1,38 @@
import React, {useCallback} from 'react';
import {
Button,
Intent,
} from '@blueprintjs/core';
import Icon from 'components/Icon';
import DialogConnect from 'connectors/Dialog.connector';
import {compose} from 'utils';
function UsersActions({
openDialog,
closeDialog,
}) {
const onClickNewUser = useCallback(() => {
openDialog('user-form');
}, []);
return (
<div claass="preferences-actions">
<Button
icon={<Icon icon='plus' iconSize={12} />}
onClick={onClickNewUser}
intent={Intent.PRIMARY}>
Invite User
</Button>
<Button
icon={<Icon icon='plus' iconSize={12} />}
onClick={onClickNewUser}>
New Role
</Button>
</div>
);
}
export default compose(
DialogConnect,
)(UsersActions);

View File

@@ -83,10 +83,7 @@ function UsersListPreferences({
};
const handleConfirmUserDelete = () => {
if (!deleteUserState) {
return;
}
if (!deleteUserState) { return; }
requestDeleteUser(deleteUserState.id).then((response) => {
setDeleteUserState(false);
AppToaster.show({

View File

@@ -49,6 +49,7 @@ const useMedia = ({ saveCallback, deleteCallback }) => {
}, [files, openProgressToast, saveCallback]);
const deleteMedia = useCallback(() => {
debugger;
return deletedFiles.length > 0
? deleteCallback(deletedFiles) : Promise.resolve();
}, [deletedFiles, deleteCallback]);

View File

@@ -27,7 +27,7 @@ export const fetchView = ({ id }) => {
export const fetchResourceViews = ({ resourceSlug }) => {
return (dispatch) => new Promise((resolve, reject) => {
ApiService.get('views', { query: { resource_name: resourceSlug } })
ApiService.get('views', { params: { resource_name: resourceSlug } })
.then((response) => {
dispatch({
type: t.RESOURCE_VIEWS_SET,

View File

@@ -6,8 +6,8 @@ export const submitMedia = ({ form, config }) => {
};
};
export const deleteMedia = ({ id }) => {
export const deleteMedia = ({ ids }) => {
return (dispatch) => {
return ApiService.delete('media', { params: { id } });
return ApiService.delete('media', { params: { ids } });
}
};

View File

@@ -45,6 +45,7 @@ $pt-font-family: Noto Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
@import 'pages/manual-journals';
@import 'pages/item-category';
@import 'pages/items';
@import 'pages/items-categories';
@import 'pages/invite-form.scss';
@import "pages/currency";
@import "pages/invite-user.scss";

View File

@@ -58,6 +58,10 @@
}
}
.bp3-button-group.bp3-minimal .bp3-button{
background-color: transparent;
}
.bp3-button{
&.bp3-intent-primary,

View File

@@ -1,7 +1,7 @@
body{
color: #444;
color: #333;
}
.#{$ns}-heading{

View File

@@ -2,7 +2,6 @@
.dashboard__insider--accounts-chart{
.bigcapital-datatable{
.normal{
.#{$ns}-icon{
color: #aaa;
@@ -17,7 +16,6 @@
padding-bottom: 0.4rem;
}
.account_name{
font-weight: 500;
.bp3-tooltip-indicator{
cursor: default;

View File

@@ -60,7 +60,6 @@
&-user{
display: flex;
align-items: center;
margin-right: 24px;
.#{$ns}-button{
background-size: contain;
@@ -247,8 +246,9 @@
h2{
font-size: 22px;
font-weight: 200;
font-weight: 300;
margin: 0;
color: #555;
}
}
}
@@ -260,8 +260,13 @@
font-size: 14px;
line-height: 50px;
font-weight: 400;
padding: 0 14px;
padding: 0;
margin-right: 0;
> a{
padding-left: 14px;
padding-right: 14px;
}
}
.#{$ns}-tab-indicator-wrapper{

View File

@@ -1,24 +1,13 @@
.dialog--invite-form {
&.bp3-dialog {
width: 400px;
width: 450px;
}
&:not(.dialog--loading) .bp3-dialog-body {
margin-bottom: 25px;
}
.bp3-dialog-body {
// margin-right: 50px;
.bp3-form-group.bp3-inline {
.bp3-label {
min-width: 70px;
}
&.form-group--email {
.bp3-form-content {
width: 250px;
}
}
}
.bp3-dialog-footer-actions {
margin-right: 30px;
display: flex;

View File

@@ -0,0 +1,8 @@
.dashboard__insider--items-categories{
.dashboard__actions-bar{
border-bottom: 2px solid #EAEAEA;
}
}

View File

@@ -1,7 +1,6 @@
.dashboard-content--preferences {
margin-left: 430px;
height: 700px;
width: 800px;
position: relative;
}
@@ -13,14 +12,34 @@
&__inside-content {
.#{$ns}-tab-list {
border-bottom: 1px solid #fd0000;
border-bottom: 1px solid #E5E5E5;
padding-left: 15px;
padding-right: 15px;
align-items: baseline;
.#{$ns}-tab {
font-weight: 300;
font-weight: 400;
line-height: 44px;
font-size: 15px;
}
}
}
&__tabs-extra-actions{
margin-left: auto;
}
&__topbar-actions{
margin-left: auto;
padding-right: 15px;
margin-right: 15px;
border-right: 1px solid #e5e5e5;
.bp3-button + .bp3-button{
margin-left: 10px;
}
}
&-menu {
width: 374px;
}
@@ -59,7 +78,8 @@
h2 {
font-size: 22px;
font-weight: 200;
font-weight: 300;
color: #555;
margin: 0;
}
}
@@ -90,7 +110,3 @@
}
}
}
.preferences__tabs-extra-actions {
position: absolute;
right: 0;
}

View File

@@ -14,7 +14,6 @@ exports.up = function (knex) {
table.text('note').nullable();
table.integer('category_id').unsigned();
table.integer('user_id').unsigned();
table.string('attachment_file');
table.timestamps();
});
};

View File

@@ -9,7 +9,7 @@ exports.up = function (knex) {
table.integer('view_id').unsigned();
}).then(() => {
return knex.seed.run({
specific: 'seed_views_role.js',
specific: 'seed_views_roles.js',
});
});
};

View File

@@ -7,13 +7,13 @@ exports.seed = function(knex) {
return knex('resource_fields').insert([
{ id: 1, label_name: 'Name', key: 'name', data_type: '', active: 1, predefined: 1 },
{ id: 2, label_name: 'Code', key: 'code', data_type: '', active: 1, predefined: 1 },
{ id: 3, label_name: 'Account Type', key: 'account_type_id', data_type: '', active: 1, predefined: 1 },
{ id: 3, label_name: 'Account Type', key: 'type', data_type: '', active: 1, predefined: 1 },
{ id: 4, label_name: 'Description', key: 'description', data_type: '', active: 1, predefined: 1 },
{ id: 5, label_name: 'Account Normal', key: 'normal', data_type: 'string', active: 1, predefined: 1 },
{
id: 6,
label_name: 'Root Account Type',
key: 'root_account_type',
key: 'root_type',
data_type: 'string',
active: 1,
predefined: 1,

View File

@@ -5,11 +5,11 @@ exports.seed = (knex) => {
.then(() => {
// Inserts seed entries
return knex('view_roles').insert([
{ id: 1, field_id: 6, comparator: 'equals', value: 'asset', view_id: 1 },
{ id: 2, field_id: 6, comparator: 'equals', value: 'liability', view_id: 2 },
{ id: 3, field_id: 6, comparator: 'equals', value: 'equity', view_id: 3 },
{ id: 4, field_id: 6, comparator: 'equals', value: 'income', view_id: 4 },
{ id: 5, field_id: 6, comparator: 'equals', value: 'expense', view_id: 5 },
{ id: 1, field_id: 6, index: 1, comparator: 'equals', value: 'asset', view_id: 1 },
{ id: 2, field_id: 6, index: 1, comparator: 'equals', value: 'liability', view_id: 2 },
{ id: 3, field_id: 6, index: 1, comparator: 'equals', value: 'equity', view_id: 3 },
{ id: 4, field_id: 6, index: 1, comparator: 'equals', value: 'income', view_id: 4 },
{ id: 5, field_id: 6, index: 1, comparator: 'equals', value: 'expense', view_id: 5 },
]);
});
};

View File

@@ -82,6 +82,7 @@ export default {
}
const form = {
custom_fields: [],
media_ids: [],
...req.body,
};
const {
@@ -90,6 +91,7 @@ export default {
ResourceField,
ItemCategory,
Item,
MediaLink,
} = req.models;
const errorReasons = [];
@@ -146,6 +148,7 @@ export default {
return res.boom.badRequest(null, { errors: errorReasons });
}
const bulkSaveMediaLinks = [];
const item = await Item.query().insertAndFetch({
name: form.name,
type: form.type,
@@ -156,6 +159,20 @@ export default {
currency_code: form.currency_code,
note: form.note,
});
form.media_ids.forEach((mediaId) => {
const oper = MediaLink.query().insert({
model_name: 'Item',
media_id: mediaId,
model_id: item.id,
});
bulkSaveMediaLinks.push(oper);
});
// Save the media links.
await Promise.all([
...bulkSaveMediaLinks,
]);
return res.status(200).send({ id: item.id });
},
},
@@ -188,14 +205,14 @@ export default {
code: 'validation_error', ...validationErrors,
});
}
const { Account, Item, ItemCategory } = req.models;
const { Account, Item, ItemCategory, MediaLink } = req.models;
const { id } = req.params;
const form = {
custom_fields: [],
...req.body,
};
const item = await Item.query().findById(id);
const item = await Item.query().findById(id).withGraphFetched('media');
if (!item) {
return res.boom.notFound(null, {
@@ -235,12 +252,12 @@ export default {
if (attachment) {
const publicPath = 'storage/app/public/';
const tenantPath = `${publicPath}${req.organizationId}`;
try {
await fsPromises.unlink(`${tenantPath}/${item.attachmentFile}`);
} catch (error) {
Logger.log('error', 'Delete item attachment file delete failed.', { error });
}
try {
await attachment.mv(`${tenantPath}/${attachment.md5}.png`);
} catch (error) {
@@ -262,6 +279,22 @@ export default {
note: form.note,
attachment_file: (attachment) ? item.attachmentFile : null,
});
// Save links of new inserted media that associated to the item model.
const itemMediaIds = item.media.map((m) => m.id);
const newInsertedMedia = difference(form.media_ids, itemMediaIds);
const bulkSaveMediaLink = [];
newInsertedMedia.forEach((mediaId) => {
const oper = MediaLink.query().insert({
model_name: 'Journal',
model_id: manualJournal.id,
media_id: mediaId,
});
bulkSaveMediaLink.push(oper);
});
await Promise.all([ ...newInsertedMedia ]);
return res.status(200).send({ id: updatedItem.id });
},
},

View File

@@ -6,6 +6,7 @@ import {
validationResult,
} from 'express-validator';
import fs from 'fs';
import { difference } from 'lodash';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import Logger from '@/services/Logger';
@@ -22,7 +23,7 @@ export default {
this.upload.validation,
asyncMiddleware(this.upload.handler));
router.delete('/delete/:id',
router.delete('/',
this.delete.validation,
asyncMiddleware(this.delete.handler));
@@ -109,7 +110,8 @@ export default {
*/
delete: {
validation: [
param('id').exists().isNumeric().toInt(),
query('ids').exists().isArray(),
query('ids.*').exists().isNumeric().toInt(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
@@ -120,26 +122,37 @@ export default {
});
}
const { Media, MediaLink } = req.models;
const { id } = req.params;
const media = await Media.query().where('id', id).first();
const ids = Array.isArray(req.query.ids) ? req.query.ids : [req.query.ids];
const media = await Media.query().whereIn('id', ids);
const mediaIds = media.map((m) => m.id);
const notFoundMedia = difference(ids, mediaIds);
if (!media) {
if (notFoundMedia.length) {
return res.status(400).send({
errors: [{ type: 'MEDIA.ID.NOT.FOUND', code: 200 }],
errors: [{ type: 'MEDIA.IDS.NOT.FOUND', code: 200, ids: notFoundMedia }],
});
}
const publicPath = 'storage/app/public/';
const tenantPath = `${publicPath}${req.organizationId}`;
const unlinkOpers = [];
try {
await fsPromises.unlink(`${tenantPath}/${media.attachmentFile}`);
Logger.log('error', 'Attachment file has been deleted.');
} catch (error) {
Logger.log('error', 'Delete item attachment file delete failed.', { error });
}
media.forEach((mediaModel) => {
const oper = fsPromises.unlink(`${tenantPath}/${mediaModel.attachmentFile}`);
unlinkOpers.push(oper);
});
await Promise.all(unlinkOpers).then((resolved) => {
resolved.forEach(() => {
Logger.log('error', 'Attachment file has been deleted.');
});
})
.catch((errors) => {
errors.forEach((error) => {
Logger.log('error', 'Delete item attachment file delete failed.', { error });
})
});
await MediaLink.query().where('media_id', media.id).delete();
await Media.query().where('id', media.id).delete();
await MediaLink.query().whereIn('media_id', mediaIds).delete();
await Media.query().whereIn('id', mediaIds).delete();
return res.status(200).send();
},

View File

@@ -12,6 +12,9 @@ export default class Item extends TenantModel {
return 'items';
}
/**
* Model modifiers.
*/
static get modifiers() {
const TABLE_NAME = Item.tableName;
@@ -29,6 +32,7 @@ export default class Item extends TenantModel {
* Relationship mapping.
*/
static get relationMappings() {
const Media = require('@/models/Media');
const Account = require('@/models/Account');
const ItemCategory = require('@/models/ItemCategory');
@@ -71,6 +75,19 @@ export default class Item extends TenantModel {
to: 'accounts.id',
},
},
media: {
relation: Model.ManyToManyRelation,
modelClass: this.relationBindKnex(Media.default),
join: {
from: 'items.id',
through: {
from: 'media_links.model_id',
to: 'media_links.media_id',
},
to: 'media.id',
}
},
};
}
}