diff --git a/backend/src/main/resources/i18n/messages.properties b/backend/src/main/resources/i18n/messages.properties index 9f3e5b20f..6173587f4 100644 --- a/backend/src/main/resources/i18n/messages.properties +++ b/backend/src/main/resources/i18n/messages.properties @@ -65,6 +65,7 @@ action.add-new-group=Add new group action.add-attribute=Add Attribute action.custom-entity-attributes=Custom Entity Attributes action.groups=Groups +action.source-group=Group value.enabled=Enabled value.disabled=Disabled @@ -491,6 +492,9 @@ label.provider=Metadata Provider message.delete-user-title=Delete User? message.delete-user-body=You are requesting to delete a user. If you complete this process the user will be removed. This cannot be undone. Do you wish to continue? +message.delete-group-title=Delete Group? +message.delete-group-body=You are requesting to delete a group. If you complete this process the group will be removed. This cannot be undone. Do you wish to continue? + message.delete-attribute-title=Delete Attribute? message.delete-attribute-body=You are requesting to delete a custom attribute. If you complete this process the attribute will be removed. This cannot be undone. Do you wish to continue? diff --git a/ui/src/app/admin/component/GroupForm.js b/ui/src/app/admin/component/GroupForm.js index 37e3b73dc..da8c5af8b 100644 --- a/ui/src/app/admin/component/GroupForm.js +++ b/ui/src/app/admin/component/GroupForm.js @@ -8,7 +8,7 @@ import Translate from '../../i18n/components/translate'; import { useGroupUiSchema } from '../hooks'; import { fields, widgets } from '../../form/component'; import { templates } from '../../form/component'; -import { FormContext, setFormDataAction } from '../../form/FormManager'; +import { FormContext, setFormDataAction, setFormErrorAction } from '../../form/FormManager'; function ErrorListTemplate() { return (<>); @@ -17,8 +17,9 @@ function ErrorListTemplate() { export function GroupForm ({group = {}, errors = [], loading = false, schema, onSave, onCancel}) { const { dispatch } = React.useContext(FormContext); - const onChange = ({formData}) => { + const onChange = ({formData, errors}) => { dispatch(setFormDataAction(formData)); + dispatch(setFormErrorAction(errors)); }; const uiSchema = useGroupUiSchema(); diff --git a/ui/src/app/admin/container/EditGroup.js b/ui/src/app/admin/container/EditGroup.js index 40e2a61e1..066350356 100644 --- a/ui/src/app/admin/container/EditGroup.js +++ b/ui/src/app/admin/container/EditGroup.js @@ -9,11 +9,16 @@ import { FormManager } from '../../form/FormManager'; import { GroupForm } from '../component/GroupForm'; import { GroupProvider } from '../hoc/GroupProvider'; +import { createNotificationAction, NotificationTypes, useNotificationDispatcher } from '../../notifications/hoc/Notifications'; +import { useTranslator } from '../../i18n/hooks'; export function EditGroup() { const { id } = useParams(); + const notifier = useNotificationDispatcher(); + const translator = useTranslator(); + const history = useHistory(); const { put, response, loading } = useGroups(); @@ -21,9 +26,16 @@ export function EditGroup() { const [blocking, setBlocking] = React.useState(false); async function save(metadata) { - await put(``, metadata); + let toast; + const resp = await put(``, metadata); if (response.ok) { gotoDetail({ refresh: true }); + toast = createNotificationAction(`Updated group successfully.`, NotificationTypes.SUCCESS); + } else { + toast = createNotificationAction(`${resp.errorCode} - ${translator(resp.errorMessage)}`, NotificationTypes.ERROR); + } + if (toast) { + notifier(toast); } }; diff --git a/ui/src/app/admin/container/GroupsList.js b/ui/src/app/admin/container/GroupsList.js index 07090f412..d86ff11b0 100644 --- a/ui/src/app/admin/container/GroupsList.js +++ b/ui/src/app/admin/container/GroupsList.js @@ -16,7 +16,7 @@ export function GroupsList({ groups, onDelete }) { } return ( - + {(block) =>
@@ -47,10 +47,10 @@ export function GroupsList({ groups, onDelete }) { - {(groups?.length > 0 ) ? groups.map((group, i) => + {( groups?.length > 0 ) ? groups.map((group, i) => {group.name} - {group.description} + {group.description || ''} @@ -66,7 +66,9 @@ export function GroupsList({ groups, onDelete }) { - ) : ''} + ) : + No groups defined. + }
diff --git a/ui/src/app/admin/container/NewGroup.js b/ui/src/app/admin/container/NewGroup.js index 1fabd0508..28994abd0 100644 --- a/ui/src/app/admin/container/NewGroup.js +++ b/ui/src/app/admin/container/NewGroup.js @@ -7,17 +7,29 @@ import { Schema } from '../../form/Schema'; import { FormManager } from '../../form/FormManager'; import { GroupForm } from '../component/GroupForm'; +import { createNotificationAction, NotificationTypes, useNotificationDispatcher } from '../../notifications/hoc/Notifications'; +import { useTranslator } from '../../i18n/hooks'; + export function NewGroup() { const history = useHistory(); + const notifier = useNotificationDispatcher(); + const translator = useTranslator(); const { post, response, loading } = useGroups({}); const [blocking, setBlocking] = React.useState(false); async function save(group) { - await post(``, group); + let toast; + const resp = await post(``, group); if (response.ok) { gotoDetail({ refresh: true }); + toast = createNotificationAction(`Added group successfully.`, NotificationTypes.SUCCESS); + } else { + toast = createNotificationAction(`${resp.errorCode} - ${translator(resp.errorMessage)}`, NotificationTypes.ERROR); + } + if (toast) { + notifier(toast); } }; diff --git a/ui/src/app/admin/hoc/GroupsProvider.js b/ui/src/app/admin/hoc/GroupsProvider.js index 256235b05..016bcee13 100644 --- a/ui/src/app/admin/hoc/GroupsProvider.js +++ b/ui/src/app/admin/hoc/GroupsProvider.js @@ -1,10 +1,14 @@ import React from 'react'; import { useGroups } from '../hooks'; +import { createNotificationAction, NotificationTypes, useNotificationDispatcher } from '../../notifications/hoc/Notifications'; +import { useTranslator } from '../../i18n/hooks'; export function GroupsProvider({ children, cache = 'no-cache' }) { const [groups, setGroups] = React.useState([]); + const notifier = useNotificationDispatcher(); + const translator = useTranslator(); const { get, del, response, loading } = useGroups({ cachePolicy: cache @@ -18,9 +22,16 @@ export function GroupsProvider({ children, cache = 'no-cache' }) { } async function removeGroup(id) { - await del(`/${id}`); + let toast; + const resp = await del(`/${id}`); if (response.ok) { loadGroups(); + toast = createNotificationAction(`Deleted group successfully.`, NotificationTypes.SUCCESS); + } else { + toast = createNotificationAction(`${resp.errorCode} - ${translator(resp.errorMessage)}`, NotificationTypes.ERROR); + } + if (toast) { + notifier(toast); } } diff --git a/ui/src/app/dashboard/view/SourcesTab.js b/ui/src/app/dashboard/view/SourcesTab.js index e6d464bff..dd0925876 100644 --- a/ui/src/app/dashboard/view/SourcesTab.js +++ b/ui/src/app/dashboard/view/SourcesTab.js @@ -32,7 +32,7 @@ export function SourcesTab () { React.useEffect(() => { loadSources() }, []); async function changeSourceGroup(source, group) { - const sources = await updater.put(`/${source.id}`, { + await updater.put(`/${source.id}`, { ...source, groupId: group }); diff --git a/ui/src/app/form/FormManager.js b/ui/src/app/form/FormManager.js index 17691970c..029c49c3c 100644 --- a/ui/src/app/form/FormManager.js +++ b/ui/src/app/form/FormManager.js @@ -57,8 +57,6 @@ function FormManager({ children, initial = {} }) { ...initialState, data }); - - const contextValue = React.useMemo(() => ({ state, dispatch }), [state, dispatch]); return ( diff --git a/ui/src/app/metadata/component/MetadataHeader.js b/ui/src/app/metadata/component/MetadataHeader.js index d8a2baa85..83ef7cee1 100644 --- a/ui/src/app/metadata/component/MetadataHeader.js +++ b/ui/src/app/metadata/component/MetadataHeader.js @@ -1,21 +1,82 @@ import React from 'react'; import FormattedDate from '../../core/components/FormattedDate'; +import { useIsAdmin } from '../../core/user/UserContext'; import Translate from '../../i18n/components/translate'; +import { GroupsProvider } from '../../admin/hoc/GroupsProvider'; +import { useMetadataEntity } from '../hooks/api'; +import { createNotificationAction, NotificationTypes, useNotificationDispatcher } from '../../notifications/hoc/Notifications'; +import { useTranslator } from '../../i18n/hooks'; +import { useMetadataLoader } from '../hoc/MetadataSelector'; + +export function MetadataHeader ({ showGroup, model, current = true, enabled = true, children, ...props }) { + + const isAdmin = useIsAdmin(); + const translator = useTranslator(); + + const { put, response } = useMetadataEntity('source', { + cachePolicy: 'no-cache' + }); + + const notifier = useNotificationDispatcher(); + + async function changeSourceGroup(s, group) { + let toast; + const resp = await put(`/${s.id}`, { + ...s, + groupId: group + }); + if (response.ok) { + toast = createNotificationAction(`Updated group successfully.`, NotificationTypes.SUCCESS); + reload(); + } else { + toast = createNotificationAction(`${resp.errorCode} - ${translator(resp.errorMessage)}`, NotificationTypes.ERROR); + } + if (toast) { + notifier(toast); + } + } + + const reload = useMetadataLoader(); -export function MetadataHeader ({ model, current = true, enabled = true, children, ...props }) { return (
- Saved:  - - - -
- By:  - {model.createdBy } +

+ Saved:  + + + +

+

+ By:  + {model.createdBy } +

+ {isAdmin && showGroup && + + {(groups, removeGroup, loadingGroups) => +
+ + + +
+ } +
+ }
{children}
@@ -29,6 +90,7 @@ export function MetadataHeader ({ model, current = true, enabled = true, childre Current

+
); diff --git a/ui/src/app/metadata/hoc/MetadataSelector.js b/ui/src/app/metadata/hoc/MetadataSelector.js index 7d7155066..acdf115be 100644 --- a/ui/src/app/metadata/hoc/MetadataSelector.js +++ b/ui/src/app/metadata/hoc/MetadataSelector.js @@ -4,6 +4,7 @@ import { useMetadataEntity } from '../hooks/api'; export const MetadataTypeContext = React.createContext(); export const MetadataObjectContext = React.createContext(); +export const MetadataLoaderContext = React.createContext(); /*eslint-disable react-hooks/exhaustive-deps*/ export function MetadataSelector({ children, ...props }) { @@ -31,10 +32,15 @@ export function MetadataSelector({ children, ...props }) { setMetadata(source); } } - React.useEffect(() => { loadMetadata(id) }, [id]); + + function reload() { + loadMetadata(id); + } + + React.useEffect(() => reload(), [id]); return ( - <> + {type && {metadata && metadata.version && @@ -42,7 +48,7 @@ export function MetadataSelector({ children, ...props }) { } } - + ); } @@ -50,4 +56,8 @@ export function useMetadataObject () { return React.useContext(MetadataObjectContext); } +export function useMetadataLoader() { + return React.useContext(MetadataLoaderContext); +} + export default MetadataSelector; \ No newline at end of file diff --git a/ui/src/app/metadata/view/MetadataOptions.js b/ui/src/app/metadata/view/MetadataOptions.js index 93011229a..38bd4a951 100644 --- a/ui/src/app/metadata/view/MetadataOptions.js +++ b/ui/src/app/metadata/view/MetadataOptions.js @@ -21,6 +21,7 @@ import { MetadataFilterTypes } from '../domain/filter'; import { useMetadataSchema } from '../hooks/schema'; import { FilterableProviders } from '../domain/provider'; + export function MetadataOptions () { const metadata = React.useContext(MetadataObjectContext); @@ -60,7 +61,8 @@ export function MetadataOptions () { + model={metadata} + showGroup={type === 'source'}> {type === 'source' && onDeleteSource &&