From bd7eb0eb7684594d1a923630e3c5f1dfa893d933 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Tue, 5 May 2020 04:21:37 +0200 Subject: [PATCH] fix issues. --- .../components/Accounts/AccountsActionsBar.js | 20 +++---- .../components/Accounts/AccountsDataTable.js | 40 +++++-------- .../components/Accounts/AccountsViewsTabs.js | 59 ++++++++++++------- client/src/components/Dashboard/Dashboard.js | 2 +- .../Dashboard/DashboardContentRoute.js | 4 +- .../Preferences/PreferencesContentRoute.js | 6 +- .../Preferences/PreferencesTopbar.js | 20 +++++++ client/src/config/preferencesMenu.js | 30 ---------- client/src/connectors/Media.connect.js | 2 +- client/src/containers/Authentication/Login.js | 7 +-- .../Dashboard/Accounts/AccountsChart.js | 16 +++-- .../Dashboard/Dialogs/AccountFormDialog.js | 10 +++- .../Dashboard/Dialogs/InviteUserDialog.js | 6 +- .../Dashboard/Dialogs/UserFormDialog.js | 33 +++++++---- .../Dashboard/Items/ItemsActionsBar.js | 27 +++++---- .../Dashboard/Items/ItemsCategoryList.js | 2 +- .../containers/Dashboard/Items/ItemsList.js | 2 +- .../Dashboard/Preferences/Currencies.js | 4 +- .../Preferences/CurrenciesActions.js | 31 ++++++++++ .../Dashboard/Preferences/CurrenciesList.js | 4 -- .../containers/Dashboard/Preferences/Users.js | 16 +---- .../Dashboard/Preferences/UsersActions.js | 38 ++++++++++++ .../Dashboard/Preferences/UsersList.js | 5 +- client/src/hooks/useMedia.js | 1 + .../store/customViews/customViews.actions.js | 2 +- client/src/store/media/media.actions.js | 4 +- client/src/style/App.scss | 1 + client/src/style/objects/buttons.scss | 4 ++ client/src/style/objects/typography.scss | 2 +- client/src/style/pages/accounts-chart.scss | 2 - client/src/style/pages/dashboard.scss | 11 +++- client/src/style/pages/invite-form.scss | 15 +---- client/src/style/pages/items-categories.scss | 8 +++ client/src/style/pages/preferences.scss | 32 +++++++--- .../20190822214303_create_items_table.js | 1 - ...20190822214905_create_views_roles_table.js | 2 +- .../database/seeds/seed_accounts_fields.js | 4 +- server/src/database/seeds/seed_views_roles.js | 10 ++-- server/src/http/controllers/Items.js | 39 +++++++++++- server/src/http/controllers/Media.js | 41 ++++++++----- server/src/models/Item.js | 17 ++++++ 41 files changed, 364 insertions(+), 216 deletions(-) create mode 100644 client/src/containers/Dashboard/Preferences/CurrenciesActions.js create mode 100644 client/src/containers/Dashboard/Preferences/UsersActions.js create mode 100644 client/src/style/pages/items-categories.scss diff --git a/client/src/components/Accounts/AccountsActionsBar.js b/client/src/components/Accounts/AccountsActionsBar.js index 3b65bced6..d8f716746 100644 --- a/client/src/components/Accounts/AccountsActionsBar.js +++ b/client/src/components/Accounts/AccountsActionsBar.js @@ -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 (); + return ( 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); diff --git a/client/src/components/Accounts/AccountsDataTable.js b/client/src/components/Accounts/AccountsDataTable.js index c32715e69..2ef09c7e4 100644 --- a/client/src/components/Accounts/AccountsDataTable.js +++ b/client/src/components/Accounts/AccountsDataTable.js @@ -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 ( - + + + ); } diff --git a/client/src/components/Accounts/AccountsViewsTabs.js b/client/src/components/Accounts/AccountsViewsTabs.js index 93dee0a20..b2937a062 100644 --- a/client/src/components/Accounts/AccountsViewsTabs.js +++ b/client/src/components/Accounts/AccountsViewsTabs.js @@ -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 = ( {view.name} + >{ view.name } ); - return ; + return ; }); + return ( All} /> - - {tabs} + id={'all'} + title={All} + onClick={handleViewLinkClick} + /> + { tabs } + ); diff --git a/client/src/containers/Dashboard/Preferences/CurrenciesActions.js b/client/src/containers/Dashboard/Preferences/CurrenciesActions.js new file mode 100644 index 000000000..5033e1ece --- /dev/null +++ b/client/src/containers/Dashboard/Preferences/CurrenciesActions.js @@ -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 ( +
+ +
+ ); +} + +export default compose( + DialogConnect, +)(CurrenciesActions); \ No newline at end of file diff --git a/client/src/containers/Dashboard/Preferences/CurrenciesList.js b/client/src/containers/Dashboard/Preferences/CurrenciesList.js index 7f4f01bbc..6554f56e7 100644 --- a/client/src/containers/Dashboard/Preferences/CurrenciesList.js +++ b/client/src/containers/Dashboard/Preferences/CurrenciesList.js @@ -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 ( - {}; - const onClickNewUser = useCallback(() => { - openDialog('user-form'); - }, [openDialog]); - return (
- + - -
- - - -
diff --git a/client/src/containers/Dashboard/Preferences/UsersActions.js b/client/src/containers/Dashboard/Preferences/UsersActions.js new file mode 100644 index 000000000..5fc4aef3b --- /dev/null +++ b/client/src/containers/Dashboard/Preferences/UsersActions.js @@ -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 ( +
+ + + +
+ ); +} + +export default compose( + DialogConnect, +)(UsersActions); \ No newline at end of file diff --git a/client/src/containers/Dashboard/Preferences/UsersList.js b/client/src/containers/Dashboard/Preferences/UsersList.js index 5a8512f91..f8f1238d1 100644 --- a/client/src/containers/Dashboard/Preferences/UsersList.js +++ b/client/src/containers/Dashboard/Preferences/UsersList.js @@ -83,10 +83,7 @@ function UsersListPreferences({ }; const handleConfirmUserDelete = () => { - if (!deleteUserState) { - return; - } - + if (!deleteUserState) { return; } requestDeleteUser(deleteUserState.id).then((response) => { setDeleteUserState(false); AppToaster.show({ diff --git a/client/src/hooks/useMedia.js b/client/src/hooks/useMedia.js index ab9326026..7e689c215 100644 --- a/client/src/hooks/useMedia.js +++ b/client/src/hooks/useMedia.js @@ -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]); diff --git a/client/src/store/customViews/customViews.actions.js b/client/src/store/customViews/customViews.actions.js index 3fc411c98..7dfc474f8 100644 --- a/client/src/store/customViews/customViews.actions.js +++ b/client/src/store/customViews/customViews.actions.js @@ -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, diff --git a/client/src/store/media/media.actions.js b/client/src/store/media/media.actions.js index 650248b29..6e680deeb 100644 --- a/client/src/store/media/media.actions.js +++ b/client/src/store/media/media.actions.js @@ -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 } }); } }; \ No newline at end of file diff --git a/client/src/style/App.scss b/client/src/style/App.scss index 305c88093..e3c6cc10c 100644 --- a/client/src/style/App.scss +++ b/client/src/style/App.scss @@ -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"; diff --git a/client/src/style/objects/buttons.scss b/client/src/style/objects/buttons.scss index 1dc1ee87c..296a758f1 100644 --- a/client/src/style/objects/buttons.scss +++ b/client/src/style/objects/buttons.scss @@ -58,6 +58,10 @@ } } +.bp3-button-group.bp3-minimal .bp3-button{ + background-color: transparent; +} + .bp3-button{ &.bp3-intent-primary, diff --git a/client/src/style/objects/typography.scss b/client/src/style/objects/typography.scss index c61b13810..7e239aa97 100644 --- a/client/src/style/objects/typography.scss +++ b/client/src/style/objects/typography.scss @@ -1,7 +1,7 @@ body{ - color: #444; + color: #333; } .#{$ns}-heading{ diff --git a/client/src/style/pages/accounts-chart.scss b/client/src/style/pages/accounts-chart.scss index 6fa2c25e5..a02a69859 100644 --- a/client/src/style/pages/accounts-chart.scss +++ b/client/src/style/pages/accounts-chart.scss @@ -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; diff --git a/client/src/style/pages/dashboard.scss b/client/src/style/pages/dashboard.scss index 85a0ec902..1cd2651e1 100644 --- a/client/src/style/pages/dashboard.scss +++ b/client/src/style/pages/dashboard.scss @@ -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{ diff --git a/client/src/style/pages/invite-form.scss b/client/src/style/pages/invite-form.scss index c391ee877..3a385f6c8 100644 --- a/client/src/style/pages/invite-form.scss +++ b/client/src/style/pages/invite-form.scss @@ -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; diff --git a/client/src/style/pages/items-categories.scss b/client/src/style/pages/items-categories.scss new file mode 100644 index 000000000..8f0534a37 --- /dev/null +++ b/client/src/style/pages/items-categories.scss @@ -0,0 +1,8 @@ + + +.dashboard__insider--items-categories{ + + .dashboard__actions-bar{ + border-bottom: 2px solid #EAEAEA; + } +} \ No newline at end of file diff --git a/client/src/style/pages/preferences.scss b/client/src/style/pages/preferences.scss index ec8e98707..2bdc9250d 100644 --- a/client/src/style/pages/preferences.scss +++ b/client/src/style/pages/preferences.scss @@ -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; -} diff --git a/server/src/database/migrations/20190822214303_create_items_table.js b/server/src/database/migrations/20190822214303_create_items_table.js index 9c0e6aa7b..c01186c82 100644 --- a/server/src/database/migrations/20190822214303_create_items_table.js +++ b/server/src/database/migrations/20190822214303_create_items_table.js @@ -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(); }); }; diff --git a/server/src/database/migrations/20190822214905_create_views_roles_table.js b/server/src/database/migrations/20190822214905_create_views_roles_table.js index c67bfadf8..740f14997 100644 --- a/server/src/database/migrations/20190822214905_create_views_roles_table.js +++ b/server/src/database/migrations/20190822214905_create_views_roles_table.js @@ -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', }); }); }; diff --git a/server/src/database/seeds/seed_accounts_fields.js b/server/src/database/seeds/seed_accounts_fields.js index 18c6d1486..2ad1974e1 100644 --- a/server/src/database/seeds/seed_accounts_fields.js +++ b/server/src/database/seeds/seed_accounts_fields.js @@ -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, diff --git a/server/src/database/seeds/seed_views_roles.js b/server/src/database/seeds/seed_views_roles.js index fe298dc56..9a8d3c649 100644 --- a/server/src/database/seeds/seed_views_roles.js +++ b/server/src/database/seeds/seed_views_roles.js @@ -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 }, ]); }); }; diff --git a/server/src/http/controllers/Items.js b/server/src/http/controllers/Items.js index 14368f200..4650b694b 100644 --- a/server/src/http/controllers/Items.js +++ b/server/src/http/controllers/Items.js @@ -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 }); }, }, diff --git a/server/src/http/controllers/Media.js b/server/src/http/controllers/Media.js index d3ae4bbb5..91ea063c8 100644 --- a/server/src/http/controllers/Media.js +++ b/server/src/http/controllers/Media.js @@ -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(); }, diff --git a/server/src/models/Item.js b/server/src/models/Item.js index f047a67a1..ee05ab169 100644 --- a/server/src/models/Item.js +++ b/server/src/models/Item.js @@ -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', + } + }, }; } }