-
+
-
-
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 (
+
+ }
+ onClick={onClickNewUser}
+ intent={Intent.PRIMARY}>
+ Invite User
+
+
+ }
+ onClick={onClickNewUser}>
+ New Role
+
+
+ );
+}
+
+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',
+ }
+ },
};
}
}