diff --git a/src/common/classes.js b/src/common/classes.js
index bea33a718..e1df0b33f 100644
--- a/src/common/classes.js
+++ b/src/common/classes.js
@@ -67,6 +67,7 @@ const CLASSES = {
PREFERENCES_PAGE_INSIDE_CONTENT_CURRENCIES: 'preferences-page__inside-content--currencies',
PREFERENCES_PAGE_INSIDE_CONTENT_ACCOUNTANT: 'preferences-page__inside-content--accountant',
PREFERENCES_PAGE_INSIDE_CONTENT_SMS_INTEGRATION: 'preferences-page__inside-content--sms-integration',
+ PREFERENCES_PAGE_INSIDE_CONTENT_ROLES_FORM: 'preferences-page__inside-content--roles-form',
FINANCIAL_REPORT_INSIDER: 'dashboard__insider--financial-report',
diff --git a/src/containers/Preferences/Users/Roles/RolesForm/RolesForm.js b/src/containers/Preferences/Users/Roles/RolesForm/RolesForm.js
new file mode 100644
index 000000000..fbbd63d51
--- /dev/null
+++ b/src/containers/Preferences/Users/Roles/RolesForm/RolesForm.js
@@ -0,0 +1,94 @@
+import React from 'react';
+import intl from 'react-intl-universal';
+import { Formik } from 'formik';
+
+import 'style/pages/Preferences/Roles/Form.scss';
+
+import { Intent } from '@blueprintjs/core';
+
+import { AppToaster, FormattedMessage as T } from 'components';
+
+import { CreateRolesFormSchema, EditRolesFormSchema } from './RolesForm.schema';
+
+import { useRolesFormContext } from './RolesFormProvider';
+
+import RolesFormContent from './RolesFormContent';
+import withDashboardActions from 'containers/Dashboard/withDashboardActions';
+
+import { compose } from 'utils';
+
+const defaultValues = {
+ role_name: 'Default',
+ role_description: '',
+ permissions: {},
+};
+
+/**
+ * Preferences - Roles Form.
+ */
+function RolesForm({
+ // #withDashboardActions
+ changePreferencesPageTitle,
+}) {
+ const { createRoleMutate, editRoleMutate, permissions } =
+ useRolesFormContext();
+
+ // Initial values.
+ const initialValues = {
+ ...defaultValues,
+ };
+
+ const MapperPermissionSchema = (data) => {
+ return data.map(({ role_name, role_description, permissions }) => {
+ const permission = _.mapKeys(permissions, (value, key) => {
+ return value;
+ });
+ return {
+ role_name: role_name,
+ role_description: role_description,
+ permissions: [permission],
+ };
+ });
+ };
+
+ React.useEffect(() => {
+ changePreferencesPageTitle();
+ }, [changePreferencesPageTitle]);
+
+ const handleFormSubmit = (values, { setSubmitting, resetForm }) => {
+ const form = {
+ ...values,
+ };
+
+ // Handle the request success.
+ const onSuccess = () => {
+ AppToaster.show({
+ message: '',
+ intent: Intent.SUCCESS,
+ });
+ };
+ // Handle the request error.
+ const onError = (
+ {
+ // response: {
+ // data: { errors },
+ // },
+ },
+ ) => {
+ setSubmitting(false);
+ };
+
+ createRoleMutate(form).then(onSuccess).catch(onError);
+ };
+
+ return (
+
+ );
+}
+
+export default compose(withDashboardActions)(RolesForm);
diff --git a/src/containers/Preferences/Users/Roles/RolesForm/RolesForm.schema.js b/src/containers/Preferences/Users/Roles/RolesForm/RolesForm.schema.js
new file mode 100644
index 000000000..dce8eb3cc
--- /dev/null
+++ b/src/containers/Preferences/Users/Roles/RolesForm/RolesForm.schema.js
@@ -0,0 +1,17 @@
+import * as Yup from 'yup';
+import intl from 'react-intl-universal';
+import { DATATYPES_LENGTH } from 'common/dataTypes';
+
+const Schema = Yup.object().shape({
+ role_name: Yup.string().label(intl.get('name')).required(),
+ role_description: Yup.string().nullable().max(DATATYPES_LENGTH.TEXT),
+
+ permissions: Yup.object().shape({
+ subject: Yup.string(),
+ ability: Yup.string(),
+ value: Yup.boolean(),
+ }),
+});
+
+export const CreateRolesFormSchema = Schema;
+export const EditRolesFormSchema = Schema;
diff --git a/src/containers/Preferences/Users/Roles/RolesForm/RolesFormContent.js b/src/containers/Preferences/Users/Roles/RolesForm/RolesFormContent.js
new file mode 100644
index 000000000..539989c6a
--- /dev/null
+++ b/src/containers/Preferences/Users/Roles/RolesForm/RolesFormContent.js
@@ -0,0 +1,72 @@
+import React from 'react';
+import { useHistory } from 'react-router-dom';
+import { ErrorMessage, FastField, Form, useFormikContext } from 'formik';
+import {
+ Button,
+ FormGroup,
+ InputGroup,
+ Intent,
+ TextArea,
+} from '@blueprintjs/core';
+import { inputIntent } from 'utils';
+import { FormattedMessage as T, FieldRequiredHint } from 'components';
+
+import { RolesPermissionList } from './components';
+
+/**
+ * Preferences - Roles Form content.
+ */
+export default function RolesFormContent() {
+ const history = useHistory();
+
+ const { isSubmitting } = useFormikContext();
+
+ const handleCloseClick = () => {
+ history.go(-1);
+ };
+
+ return (
+
+ );
+}
diff --git a/src/containers/Preferences/Users/Roles/RolesForm/RolesFormPage.js b/src/containers/Preferences/Users/Roles/RolesForm/RolesFormPage.js
new file mode 100644
index 000000000..a263d2942
--- /dev/null
+++ b/src/containers/Preferences/Users/Roles/RolesForm/RolesFormPage.js
@@ -0,0 +1,15 @@
+import React from 'react';
+
+import RolesForm from './RolesForm';
+import { RolesFormProvider } from './RolesFormProvider';
+
+/**
+ * Roles Form page.
+ */
+export default function RolesFormPage() {
+ return (
+
+
+
+ );
+}
diff --git a/src/containers/Preferences/Users/Roles/RolesForm/RolesFormProvider.js b/src/containers/Preferences/Users/Roles/RolesForm/RolesFormProvider.js
new file mode 100644
index 000000000..73f416b51
--- /dev/null
+++ b/src/containers/Preferences/Users/Roles/RolesForm/RolesFormProvider.js
@@ -0,0 +1,60 @@
+import React from 'react';
+import { flatMap, map } from 'lodash';
+import classNames from 'classnames';
+import { CLASSES } from 'common/classes';
+
+import {
+ useCreateRolePermissionSchema,
+ useEditRole,
+ usePermissionsSchema,
+} from 'hooks/query';
+import PreferencesPageLoader from '../../../PreferencesPageLoader';
+
+const RolesFormContext = React.createContext();
+
+/**
+ * Roles Form page provider.
+ */
+function RolesFormProvider({ ...props }) {
+ // Create and edit roles mutations.
+ const { mutateAsync: createRolePermissionMutate } =
+ useCreateRolePermissionSchema();
+ const { mutateAsync: editRolePermissionMutate } =
+ useEditRolePermissionSchema();
+
+ const {
+ data: permissionsSchema,
+ isLoading: isPermissionsSchemaLoading,
+ isFetching: isPermissionsSchemaFetching,
+ } = usePermissionsSchema();
+
+ // Provider state.
+ const provider = {
+ permissionsSchema,
+ isPermissionsSchemaLoading,
+ isPermissionsSchemaFetching,
+ createRolePermissionMutate,
+ editRolePermissionMutate,
+ };
+
+ return (
+
+
+ {isPermissionsSchemaLoading ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
+
+const useRolesFormContext = () => React.useContext(RolesFormContext);
+
+export { RolesFormProvider, useRolesFormContext };
diff --git a/src/containers/Preferences/Users/Roles/RolesForm/components.js b/src/containers/Preferences/Users/Roles/RolesForm/components.js
new file mode 100644
index 000000000..f7ef4bbd6
--- /dev/null
+++ b/src/containers/Preferences/Users/Roles/RolesForm/components.js
@@ -0,0 +1,150 @@
+import React from 'react';
+import { Checkbox } from '@blueprintjs/core';
+import styled from 'styled-components';
+import { castArray } from 'lodash';
+
+import { FastField, useFormikContext } from 'formik';
+import { whenRtl, whenLtr } from 'utils/styled-components';
+import { Icon, Hint, If, Choose } from 'components';
+import { useRolesFormContext } from './RolesFormProvider';
+
+const RoleLabelCheckbox = ({ subject, label, description }) => (
+ <>
+
+ {/*------------- subject checbox ------------- */}
+
+ {({ form: { setFieldValue, values }, field }) => (
+
+ )}
+
+ {description}
+
+ >
+);
+
+const AbilitiesList = ({ subject, abilities }) => {
+ return (
+
+ {abilities?.map(({ key, label }) => (
+
+ {({ form: { setFieldValue, values }, field }) => (
+
+ )}
+
+ ))}
+
+ );
+};
+
+const ExtraAbilitiesList = ({ subject, extraAbilities }) => {
+ return extraAbilities?.map(({ key, label }) => (
+
+
+ {({ form: { setFieldValue, values }, field }) => (
+
+ )}
+
+
+ ));
+};
+
+export const RolesPermissionList = () => {
+ const { permissionsSchema } = useRolesFormContext();
+
+ return (
+
+
+ {permissionsSchema.map(
+ ({
+ subject,
+ subject_label,
+ description,
+ abilities,
+ extra_abilities,
+ }) => {
+ const extraAbilitiesList = Array.isArray(extra_abilities)
+ ? extra_abilities
+ : [];
+
+ const abilitiesList = castArray(abilities) ? abilities : [];
+
+ return (
+
+
+
+
+
+
+
+
+ );
+ },
+ )}
+
+
+ );
+};
+
+const GroupList = styled.div`
+ list-style: none;
+ border: 1px solid #d2dce2;
+ border-radius: 6px;
+ font-size: 13px;
+
+ ul:first-child > li:last-child {
+ border-bottom: 0;
+ border-top: 0;
+ }
+`;
+
+const BoxedGroupList = styled.ul`
+ margin: 0;
+ list-style: none;
+`;
+
+const RoleList = styled.li`
+ display: block;
+ padding: 5px 10px;
+ margin: 0;
+ line-height: 20px;
+ border-bottom: 1px solid #e0e0e0;
+`;
+
+const LabelCheckbox = styled.label`
+ > * {
+ display: inline-block;
+ }
+ .block {
+ width: 220px;
+ padding: 2px 0;
+ font-weight: 500;
+ }
+`;
+
+const AbilitieList = styled.ul`
+ list-style: none;
+ /* margin-left: 12px; // 10px */
+ margin: 0px 10px 0px;
+
+ > li {
+ display: inline-block;
+ margin-top: 3px;
+ }
+`;
+
+const AbilitiesChildList = styled.li`
+ display: inline-block;
+ margin-top: 3px;
+`;
diff --git a/src/containers/Preferences/Users/Roles/RolesLanding/RolesDataTable.js b/src/containers/Preferences/Users/Roles/RolesLanding/RolesDataTable.js
new file mode 100644
index 000000000..775f98970
--- /dev/null
+++ b/src/containers/Preferences/Users/Roles/RolesLanding/RolesDataTable.js
@@ -0,0 +1,28 @@
+import React from 'react';
+import intl from 'react-intl-universal';
+
+import { DataTable } from 'components';
+import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
+
+import { useRolesTableColumns, ActionsMenu } from './components';
+
+import { compose } from 'utils';
+
+/**
+ * Roles data table.
+ */
+export default function RolesDataTable() {
+ const columns = useRolesTableColumns();
+
+ return (
+
+ );
+}
diff --git a/src/containers/Preferences/Users/Roles/RolesLanding/RolesList.js b/src/containers/Preferences/Users/Roles/RolesLanding/RolesList.js
new file mode 100644
index 000000000..436adb6e5
--- /dev/null
+++ b/src/containers/Preferences/Users/Roles/RolesLanding/RolesList.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import intl from 'react-intl-universal';
+
+import { RolesListProvider } from './RolesListProvider';
+import RolesDataTable from './RolesDataTable';
+
+/**
+ * Roles list.
+ */
+function RolesListPrefernces() {
+ return (
+
+
+
+ );
+}
+
+export default RolesListPrefernces;
diff --git a/src/containers/Preferences/Users/Roles/RolesLanding/RolesListProvider.js b/src/containers/Preferences/Users/Roles/RolesLanding/RolesListProvider.js
new file mode 100644
index 000000000..248b47260
--- /dev/null
+++ b/src/containers/Preferences/Users/Roles/RolesLanding/RolesListProvider.js
@@ -0,0 +1,28 @@
+import React from 'react';
+import classNames from 'classnames';
+import { CLASSES } from 'common/classes';
+// import {} from 'hooks/query';
+
+const RolesListContext = React.createContext();
+
+/**
+ * Roles list provider.
+ */
+function RolesListProvider({ ...props }) {
+ // Provider state.
+ const provider = {};
+ return (
+
+
+
+ );
+}
+
+const useRolesContext = () => React.useContext(RolesListContext);
+
+export { RolesListProvider, useRolesContext };
diff --git a/src/containers/Preferences/Users/Roles/RolesLanding/components.js b/src/containers/Preferences/Users/Roles/RolesLanding/components.js
new file mode 100644
index 000000000..350cfa34b
--- /dev/null
+++ b/src/containers/Preferences/Users/Roles/RolesLanding/components.js
@@ -0,0 +1,54 @@
+import React from 'react';
+import intl from 'react-intl-universal';
+
+import { Intent, Button, Menu, MenuItem, MenuDivider } from '@blueprintjs/core';
+import { safeInvoke } from 'utils';
+import { Icon, If } from 'components';
+
+/**
+ * Context menu of roles.
+ */
+export function ActionsMenu({ payload: {}, row: { original } }) {
+ return (
+
+ );
+}
+
+/**
+ * Retrieve Roles table columns.
+ * @returns
+ */
+export function useRolesTableColumns() {
+ return React.useMemo(
+ () => [
+ {
+ id: 'name',
+ Header: intl.get('roles.column.name'),
+ // accessor: ,
+ className: 'name',
+ width: '100',
+ disableSortBy: true,
+ },
+ {
+ id: 'description',
+ Header: intl.get('roles.column.description'),
+ // accessor: ,
+ className: 'description',
+ width: '120',
+ disableSortBy: true,
+ },
+ ],
+ [],
+ );
+}
diff --git a/src/containers/Preferences/Users/RolesList.js b/src/containers/Preferences/Users/RolesList.js
deleted file mode 100644
index e69de29bb..000000000
diff --git a/src/containers/Preferences/Users/Users.js b/src/containers/Preferences/Users/Users.js
index ed5e9a806..4f453d0b6 100644
--- a/src/containers/Preferences/Users/Users.js
+++ b/src/containers/Preferences/Users/Users.js
@@ -26,11 +26,18 @@ function UsersPreferences({ openDialog }) {
);
diff --git a/src/containers/Preferences/Users/UsersActions.js b/src/containers/Preferences/Users/UsersActions.js
index ada5c76af..3575b6ee9 100644
--- a/src/containers/Preferences/Users/UsersActions.js
+++ b/src/containers/Preferences/Users/UsersActions.js
@@ -1,40 +1,41 @@
import React from 'react';
-import {
- Button,
- Intent,
-} from '@blueprintjs/core';
+import { useHistory } from 'react-router-dom';
+
+import { Button, Intent } from '@blueprintjs/core';
import { FormattedMessage as T } from 'components';
import Icon from 'components/Icon';
import withDialogActions from 'containers/Dialog/withDialogActions';
-import {compose} from 'utils';
+import { compose } from 'utils';
-function UsersActions({
- openDialog,
- closeDialog,
-}) {
+function UsersActions({ openDialog, closeDialog }) {
+ const history = useHistory();
const onClickNewUser = () => {
openDialog('invite-user');
};
+ const onClickNewRole = () => {
+ history.push('/preferences/roles');
+ };
+
return (
}
+ icon={}
onClick={onClickNewUser}
- intent={Intent.PRIMARY}>
+ intent={Intent.PRIMARY}
+ >
}
- onClick={onClickNewUser}>
+ icon={}
+ onClick={onClickNewRole}
+ >
);
}
-export default compose(
- withDialogActions,
-)(UsersActions);
\ No newline at end of file
+export default compose(withDialogActions)(UsersActions);
diff --git a/src/hooks/query/index.js b/src/hooks/query/index.js
index ca18d976d..e73c09307 100644
--- a/src/hooks/query/index.js
+++ b/src/hooks/query/index.js
@@ -29,3 +29,4 @@ export * from './GenericResource';
export * from './jobs';
export * from './misc';
export * from './cashflowAccounts'
+export * from './roles'
diff --git a/src/hooks/query/roles.js b/src/hooks/query/roles.js
new file mode 100644
index 000000000..af514238c
--- /dev/null
+++ b/src/hooks/query/roles.js
@@ -0,0 +1,77 @@
+import { useMutation, useQueryClient } from 'react-query';
+import { useRequestQuery } from '../useQueryRequest';
+import useApiRequest from '../useRequest';
+import t from './types';
+
+// Common invalidate queries.
+const commonInvalidateQueries = (queryClient) => {
+ queryClient.invalidateQueries(t.ROLES_PERMISSIONS_SCHEMA);
+ queryClient.invalidateQueries(t.ROLE);
+};
+
+/**
+ * Edit role .
+ */
+export function useEditRolePermissionSchema(props) {
+ const queryClient = useQueryClient();
+ const apiRequest = useApiRequest();
+
+ return useMutation(([id, values]) => apiRequest.post(`roles/${id}`, values), {
+ onSuccess: () => {
+ // Common invalidate queries.
+ commonInvalidateQueries(queryClient);
+ },
+ ...props,
+ });
+}
+
+/**
+ * Create a new roles
+ */
+export function useCreateRolePermissionSchema(props) {
+ const queryClient = useQueryClient();
+ const apiRequest = useApiRequest();
+
+ return useMutation((values) => apiRequest.post(`roles`, values), {
+ onSuccess: () => {
+ // Common invalidate queries.
+ commonInvalidateQueries(queryClient);
+ },
+ ...props,
+ });
+}
+
+/**
+ * Delete the given role.
+ */
+export function useDeleteRole(props) {
+ const queryClient = useQueryClient();
+ const apiRequest = useApiRequest();
+
+ return useMutation((id) => apiRequest.delete(`roles/${id}`), {
+ onSuccess: (res, id) => {
+ // Invalidate specific role.
+ queryClient.invalidateQueries(t.ROLE, id);
+
+ commonInvalidateQueries(queryClient);
+ },
+ ...props,
+ });
+}
+
+/**
+ * Retrive the roles permissions schema.
+ */
+export function usePermissionsSchema(query, props) {
+ return useRequestQuery(
+ [t.ROLES_PERMISSIONS_SCHEMA, query],
+ { method: 'get', url: 'roles/permissions/schema', params: query },
+ {
+ select: (res) => res.data.data,
+ defaultData: {
+ roles: [],
+ },
+ ...props,
+ },
+ );
+}
diff --git a/src/hooks/query/types.js b/src/hooks/query/types.js
index 1aebbd8e1..edec96183 100644
--- a/src/hooks/query/types.js
+++ b/src/hooks/query/types.js
@@ -102,6 +102,12 @@ const USERS = {
USER: 'USER',
};
+const ROLES = {
+ ROLE: 'ROLE',
+ ROLES: 'ROLES',
+ ROLES_PERMISSIONS_SCHEMA: 'ROLES_PERMISSIONS_SCHEMA',
+};
+
const SETTING = {
SETTING: 'SETTING',
SETTING_INVOICES: 'SETTING_INVOICES',
@@ -177,4 +183,5 @@ export default {
...LANDED_COSTS,
...CONTACTS,
...CASH_FLOW_ACCOUNTS,
+ ...ROLES,
};
diff --git a/src/routes/preferences.js b/src/routes/preferences.js
index 98ff2e46b..1c7021654 100644
--- a/src/routes/preferences.js
+++ b/src/routes/preferences.js
@@ -1,5 +1,6 @@
import General from 'containers/Preferences/General/General';
-import Users from 'containers/Preferences/Users/Users';
+import Users from '../containers/Preferences/Users/Users';
+import Roles from '../containers/Preferences/Users/Roles/RolesForm/RolesFormPage';
import Accountant from 'containers/Preferences/Accountant/Accountant';
// import Accounts from 'containers/Preferences/Accounts/Accounts';
import Currencies from 'containers/Preferences/Currencies/Currencies';
@@ -20,6 +21,11 @@ export default [
component: Users,
exact: true,
},
+ {
+ path: `${BASE_URL}/roles`,
+ component: Roles,
+ exact: true,
+ },
{
path: `${BASE_URL}/currencies`,
component: Currencies,
diff --git a/src/routes/preferencesTabs.js b/src/routes/preferencesTabs.js
index 48537aa24..32b377899 100644
--- a/src/routes/preferencesTabs.js
+++ b/src/routes/preferencesTabs.js
@@ -1,30 +1,20 @@
// import AccountsCustomFields from "containers/Preferences/AccountsCustomFields";
-import UsersList from 'containers/Preferences/Users/UsersList';
-import RolesList from 'containers/Preferences/Users/RolesList';
+import UsersList from '../containers/Preferences/Users/UsersList';
+import RolesList from '../containers/Preferences/Users/Roles/RolesLanding/RolesList';
export default {
- accounts: [
- // {
- // path: '',
- // component: AccountsCustomFields,
- // exact: true,
- // },
- // {
- // path: 'custom_fields',
- // component: AccountsCustomFields,
- // exact: true,
- // },
- ],
users: [
{
path: '',
component: UsersList,
exact: true,
},
+ ],
+ roles: [
{
- path: '/roles',
+ path: '',
component: RolesList,
exact: true,
},
],
-}
\ No newline at end of file
+};
diff --git a/src/style/pages/Preferences/Roles/Form.scss b/src/style/pages/Preferences/Roles/Form.scss
new file mode 100644
index 000000000..916d611c2
--- /dev/null
+++ b/src/style/pages/Preferences/Roles/Form.scss
@@ -0,0 +1,58 @@
+// Roles Form page
+//---------------------------------
+.preferences-page__inside-content--roles-form {
+ .card {
+ padding: 25px;
+
+ .card__footer {
+ padding-top: 16px;
+ border-top: 1px solid #e0e7ea;
+ margin-top: 30px;
+
+ .bp3-button {
+ min-width: 65px;
+
+ + .bp3-button {
+ margin-left: 10px;
+ }
+ }
+ }
+ }
+
+ .bp3-form-group {
+ max-width: 500px;
+ margin-bottom: 14px;
+
+ &.bp3-inline {
+ .bp3-label {
+ min-width: 100px;
+ }
+ }
+ .bp3-form-content {
+ width: 100%;
+ }
+ }
+ .form-group--description {
+ textarea {
+ width: 100%;
+ min-width: 100%;
+ font-size: 14px;
+ }
+ }
+ .bp3-control.bp3-checkbox {
+ .bp3-control-indicator {
+ border: 1px solid #c2c2c2;
+ cursor: auto;
+
+ &,
+ &:hover {
+ height: 15px;
+ width: 15px;
+ }
+ &:before {
+ width: 15px;
+ height: 15px;
+ }
+ }
+ }
+}
diff --git a/src/style/pages/Preferences/Users.scss b/src/style/pages/Preferences/Users.scss
index 9046fb9f1..c9a3726e7 100644
--- a/src/style/pages/Preferences/Users.scss
+++ b/src/style/pages/Preferences/Users.scss
@@ -1,12 +1,9 @@
-
// Users/Roles List.
// ---------------------------------
-.preferences-page__inside-content--users{
-
-.bigcapital-datatable {
-
- .td{
- .avatar{
+.preferences-page__inside-content--users {
+ .bigcapital-datatable {
+ .td {
+ .avatar {
display: block;
height: 28px;
width: 28px;
@@ -23,8 +20,13 @@
text-transform: uppercase;
}
- .tr:last-child .td{
+ .tr:last-child .td {
border-bottom: 0;
}
}
+ .bp3-tabs {
+ .bp3-tab-panel {
+ margin-top: 0;
+ }
+ }
}