From 70bcc94e7fc824a097fb88a194312a0c65f2eecd Mon Sep 17 00:00:00 2001 From: Norbert Csaba Herczeg Date: Wed, 20 Sep 2023 20:25:42 +0200 Subject: [PATCH] JNG-5211 bulk operations 2 --- .../model/OperationFaultTest-ui.model | 300 ++++++++++++++++++ .../operation_fault_test__actor/pom.xml | 183 +++++++++++ .../OperationFaultTest/pom.xml | 26 ++ judo-ui-react-itest/pom.xml | 1 + .../ui/generator/react/UiTableHelper.java | 14 +- .../actor/public/i18n/system_default.json.hbs | 2 + .../actor/public/i18n/system_en-US.json.hbs | 2 + .../actor/public/i18n/system_hu-HU.json.hbs | 2 + .../actor/src/hooks/useCRUDDialog.tsx.hbs | 118 ++++--- .../action/call-operation-action.tsx.hbs | 2 +- .../without-input-form.fragment.hbs | 98 +++--- .../components/table/for-aggregation.hbs | 41 +++ .../components/table/for-association.hbs | 36 +++ .../pages/components/table/for-table-page.hbs | 41 +++ pom.xml | 2 +- 15 files changed, 784 insertions(+), 84 deletions(-) create mode 100644 judo-ui-react-itest/OperationFaultTest/model/OperationFaultTest-ui.model create mode 100644 judo-ui-react-itest/OperationFaultTest/operation_fault_test__actor/pom.xml create mode 100644 judo-ui-react-itest/OperationFaultTest/pom.xml diff --git a/judo-ui-react-itest/OperationFaultTest/model/OperationFaultTest-ui.model b/judo-ui-react-itest/OperationFaultTest/model/OperationFaultTest-ui.model new file mode 100644 index 00000000..b4023c49 --- /dev/null +++ b/judo-ui-react-itest/OperationFaultTest/model/OperationFaultTest-ui.model @@ -0,0 +1,300 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + View + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + View + + + + + + + + + + + + + + + + + + + + + + + + + + + + View + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + View + + + + + + + + + + + + + + + + + + + View + + + + + + + + + + + + + + + + + + + + + + + + View + + + + + + + + + + + + + + + + + + + View + + + + + + + + + LIST + CREATE + VALIDATE_CREATE + REFRESH + UPDATE + VALIDATE_UPDATE + DELETE + + + + + + + + + + + + + + + + + + + + REFRESH + UPDATE + VALIDATE_UPDATE + DELETE + TEMPLATE + View + + + + TEMPLATE + View + + + + View + + + + + + View + + + + View + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/judo-ui-react-itest/OperationFaultTest/operation_fault_test__actor/pom.xml b/judo-ui-react-itest/OperationFaultTest/operation_fault_test__actor/pom.xml new file mode 100644 index 00000000..601058ea --- /dev/null +++ b/judo-ui-react-itest/OperationFaultTest/operation_fault_test__actor/pom.xml @@ -0,0 +1,183 @@ + + 4.0.0 + + + hu.blackbelt.judo.generator + operationfaulttest-application-frontend-react + ${revision} + + operationfaulttest-application-frontend-react-operation_fault_test__actor + OperationFaultTest - Actor frontend react + OperationFaultTest - Actor react frontend + + bundle + + + operationfaulttest__actor + actor + Actor + OperationFaultTest::Actor + + ${project.parent.basedir}/model/${model-name}-ui.model + + ${project.parent.parent.parent.basedir}/.nodejs + ${basedir}/target/frontend-react + + + + + + org.apache.felix + maven-bundle-plugin + 5.1.8 + true + + + /${model-name}/${actor} + + /=${generation-target}/dist + + + + + + + hu.blackbelt.judo.meta + judo-ui-generator-maven-plugin + ${judo-meta-ui-version} + + + execute-ui-services-generation + generate-sources + + generate + + + + mvn:hu.blackbelt.judo.generator:judo-ui-typescript-rest-api:${judo-ui-typescript-rest-version} + mvn:hu.blackbelt.judo.generator:judo-ui-typescript-rest-service:${judo-ui-typescript-rest-version} + mvn:hu.blackbelt.judo.generator:judo-ui-typescript-rest-axios:${judo-ui-typescript-rest-version} + + ui-typescript-rest + + hu.blackbelt.judo.generator.commons, + hu.blackbelt.judo.ui.generator.typescript.rest + + + ${actor-fq-name} + + ${ui-model} + ${generation-target}/src/generated + + + + execute-ui-generation + generate-sources + + generate + + + + mvn:hu.blackbelt.judo.generator:judo-ui-react:${revision} + + ui-react + + hu.blackbelt.judo.generator.commons, + hu.blackbelt.judo.ui.generator.react, + hu.blackbelt.judo.ui.generator.typescript.rest.commons + + + ${actor-fq-name} + + ${ui-model} + ${generation-target} + + true + ${model-name} + ${appScope} + ${appVersion} + + ${defaultLanguage} + ${tablePageLimit} + + ${muiLicensePlan} + + + + + + + hu.blackbelt.judo.generator + judo-ui-typescript-rest-commons + ${judo-ui-typescript-rest-version} + + + hu.blackbelt.judo.generator + judo-ui-typescript-rest-api + ${judo-ui-typescript-rest-version} + + + hu.blackbelt.judo.generator + judo-ui-typescript-rest-service + ${judo-ui-typescript-rest-version} + + + hu.blackbelt.judo.generator + judo-ui-typescript-rest-axios + ${judo-ui-typescript-rest-version} + + + hu.blackbelt.judo.generator + judo-ui-react + ${revision} + + + + + + com.github.eirslett + frontend-maven-plugin + ${frontend-maven-plugin-version} + + + pnpm install + + pnpm + + generate-sources + + install + + + + + format code + + pnpm + + generate-sources + + run format + + + + + build + + pnpm + + generate-sources + + run build + + + + + ${node-install-dir} + ${generation-target} + + + + + diff --git a/judo-ui-react-itest/OperationFaultTest/pom.xml b/judo-ui-react-itest/OperationFaultTest/pom.xml new file mode 100644 index 00000000..853ba112 --- /dev/null +++ b/judo-ui-react-itest/OperationFaultTest/pom.xml @@ -0,0 +1,26 @@ + + + 4.0.0 + + hu.blackbelt.judo.generator + judo-ui-react-itest + ${revision} + + operationfaulttest-application-frontend-react + ${revision} + + JUDO UI React Frontend Generator ITest - OperationFaultTest + + pom + + + OperationFaultTest + ${basedir}/generator-overrides + ${project.parent.parent.basedir}/.nodejs + + + + operation_fault_test__actor + + diff --git a/judo-ui-react-itest/pom.xml b/judo-ui-react-itest/pom.xml index a1517a8a..00090826 100644 --- a/judo-ui-react-itest/pom.xml +++ b/judo-ui-react-itest/pom.xml @@ -36,6 +36,7 @@ CRUDActionsTest FormsTest MultiPrincipalTest + OperationFaultTest OperationParametersTest ReadOnlyTest RelationTestReckless diff --git a/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiTableHelper.java b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiTableHelper.java index 08220f30..d44b841a 100644 --- a/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiTableHelper.java +++ b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiTableHelper.java @@ -26,7 +26,7 @@ import lombok.extern.slf4j.Slf4j; import java.util.Collection; -import java.util.Optional; +import java.util.Comparator; @Slf4j @TemplateHelper @@ -248,4 +248,16 @@ public static String getSortDirection(Column column) { return "null"; } } + + public static java.util.List getBulkOperationActionsForTable(Table table) { + return table.getRowActions().stream() + .filter(Action::getIsCallOperationAction) + .filter(Action::isIsBulk) + .sorted(Comparator.comparing(NamedElement::getFQName)) + .toList(); + } + + public static boolean tableHasBulkOperations(Table table) { + return !getBulkOperationActionsForTable(table).isEmpty(); + } } diff --git a/judo-ui-react/src/main/resources/actor/public/i18n/system_default.json.hbs b/judo-ui-react/src/main/resources/actor/public/i18n/system_default.json.hbs index 6a66d0d2..fa38d968 100644 --- a/judo-ui-react/src/main/resources/actor/public/i18n/system_default.json.hbs +++ b/judo-ui-react/src/main/resources/actor/public/i18n/system_default.json.hbs @@ -97,6 +97,8 @@ "judo.modal.filter.equals": "equals", "judo.modal.filter.notEquals": "notEquals", "judo.error.error": "Error", + "judo.error.unhandled": "An unhandled error occurred.", + "judo.error.unmappable": "An error occurred, but we could not display the error info.", "judo.error.internal-server-error": "An internal server error occurred.", "judo.error.technical": "Something went wrong. Please contact with the system admins.", "judo.error.technical.no-response": "No response received while processing your request. Please contact with the system admins.", diff --git a/judo-ui-react/src/main/resources/actor/public/i18n/system_en-US.json.hbs b/judo-ui-react/src/main/resources/actor/public/i18n/system_en-US.json.hbs index 6a66d0d2..fa38d968 100644 --- a/judo-ui-react/src/main/resources/actor/public/i18n/system_en-US.json.hbs +++ b/judo-ui-react/src/main/resources/actor/public/i18n/system_en-US.json.hbs @@ -97,6 +97,8 @@ "judo.modal.filter.equals": "equals", "judo.modal.filter.notEquals": "notEquals", "judo.error.error": "Error", + "judo.error.unhandled": "An unhandled error occurred.", + "judo.error.unmappable": "An error occurred, but we could not display the error info.", "judo.error.internal-server-error": "An internal server error occurred.", "judo.error.technical": "Something went wrong. Please contact with the system admins.", "judo.error.technical.no-response": "No response received while processing your request. Please contact with the system admins.", diff --git a/judo-ui-react/src/main/resources/actor/public/i18n/system_hu-HU.json.hbs b/judo-ui-react/src/main/resources/actor/public/i18n/system_hu-HU.json.hbs index 74e008b4..949acf01 100644 --- a/judo-ui-react/src/main/resources/actor/public/i18n/system_hu-HU.json.hbs +++ b/judo-ui-react/src/main/resources/actor/public/i18n/system_hu-HU.json.hbs @@ -97,6 +97,8 @@ "judo.applications.available_applications": "Választható alkalmazások", "judo.applications.change": "Alkalmazás váltás", "judo.error.error": "Hiba", + "judo.error.unhandled": "Ismeretlen hiba történt.", + "judo.error.unmappable": "Hiba történt, nem tudjuk megjeleníteni a választ.", "judo.error.internal-server-error": "Belső alkalmazás hiba történt", "judo.error.technical": "Azonosítatlan hiba történt. Kérjük lépjen kapcsolatba a rendszer üzemeltetőivel!", "judo.error.technical.no-response": "Nem érkezett válasz a műveletre. Kérjük lépjen kapcsolatba a rendszer üzemeltetőivel!", diff --git a/judo-ui-react/src/main/resources/actor/src/hooks/useCRUDDialog.tsx.hbs b/judo-ui-react/src/main/resources/actor/src/hooks/useCRUDDialog.tsx.hbs index 5bfb48e7..3bb58edf 100644 --- a/judo-ui-react/src/main/resources/actor/src/hooks/useCRUDDialog.tsx.hbs +++ b/judo-ui-react/src/main/resources/actor/src/hooks/useCRUDDialog.tsx.hbs @@ -1,14 +1,29 @@ -import {useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { ReactNode } from 'react'; import type { JudoStored } from '@judo/data-api-common'; import { useTranslation } from 'react-i18next'; import { LoadingButton } from '@mui/lab'; -import { DialogContent, Grid, DialogTitle, IconButton, Box, LinearProgress, Typography, List, ListItem, ListItemIcon, ListItemText, DialogActions, Button } from '@mui/material'; +import { + DialogContent, + Grid, + DialogTitle, + IconButton, + Box, + LinearProgress, + Typography, + List, + ListItem, + ListItemIcon, + ListItemText, + DialogActions, + Button, +} from '@mui/material'; import type { LinearProgressProps } from '@mui/material'; import { useSnackbar } from 'notistack'; import { useDialog } from '~/components/dialog'; import { MdiIcon } from '~/components'; import { toastConfig } from '~/config'; +import { isErrorOperationFault } from '~/utilities'; export type CRUDDialogOpenProps> = { dialogTitle: string; @@ -17,6 +32,7 @@ export type CRUDDialogOpenProps> = { action: (item: T, successHandler: () => void, errorHandler: (error: any) => void) => Promise; onClose: (needsRefresh: boolean) => void; autoCloseOnSuccess?: boolean; + faultPrefix?: string; }; export type UseCRUDDialog = () => >(props: CRUDDialogOpenProps) => void; @@ -24,7 +40,7 @@ export type UseCRUDDialog = () => >(props: CRUDDialogO export const useCRUDDialog: UseCRUDDialog = () => { const [createDialog, closeDialog] = useDialog(); - return ({ dialogTitle, itemTitleFn, selectedItems, action, onClose, autoCloseOnSuccess }): void => { + return ({ dialogTitle, itemTitleFn, selectedItems, action, onClose, autoCloseOnSuccess, faultPrefix }): void => { createDialog({ fullWidth: true, maxWidth: 'sm', @@ -49,6 +65,7 @@ export const useCRUDDialog: UseCRUDDialog = () => { onClose(needsRefresh); } } autoCloseOnSuccess={autoCloseOnSuccess} + faultPrefix={faultPrefix} /> ), }); @@ -63,17 +80,18 @@ const iconMapping: Record = { error: , }; -export type QueueItem = { id: string, title: string, status: ItemStatus, data: any }; +export type QueueItem = { id: string; title: string; status: ItemStatus; data: any, error?: any }; export type CRUDDialogProps = { - title: string, - close: (needsRefresh: boolean) => void, - queueItems: Array, + title: string; + close: (needsRefresh: boolean) => void; + queueItems: Array; action: (item: any, successHandler: () => void, errorHandler: (error: any) => void) => Promise; autoCloseOnSuccess?: boolean; -} + faultPrefix?: string; +}; -export function CRUDDialog({ title, close, queueItems, action, autoCloseOnSuccess }: CRUDDialogProps) { +export function CRUDDialog({ title, close, queueItems, action, autoCloseOnSuccess, faultPrefix }: CRUDDialogProps) { const MAX_PROGRESS = 100; const { enqueueSnackbar } = useSnackbar(); const { t } = useTranslation(); @@ -83,16 +101,20 @@ export function CRUDDialog({ title, close, queueItems, action, autoCloseOnSucces const [queue, setQueue] = useState>([...queueItems]); const progressMultiplier = useMemo(() => Math.round(MAX_PROGRESS / queueItems.length), [queueItems]); - const updateQueueItem = useCallback((queueItem: QueueItem, status: ItemStatus) => { - setQueue((prevQueue) => { - const idx = prevQueue.findIndex(i => i.id === queueItem.id); - prevQueue[idx].status = status; - return [...prevQueue]; - }); - if (status === 'success') { - setProgress((prevProgress) => prevProgress + progressMultiplier); - } - }, [queueItems]); + const updateQueueItem = useCallback( + (queueItem: QueueItem, status: ItemStatus, error?: any) => { + setQueue((prevQueue) => { + const idx = prevQueue.findIndex((i) => i.id === queueItem.id); + prevQueue[idx].status = status; + prevQueue[idx].error = error; + return [...prevQueue]; + }); + if (status === 'success') { + setProgress((prevProgress) => prevProgress + progressMultiplier); + } + }, + [queueItems], + ); const runActions = useCallback(async () => { if (inProgress) { @@ -109,21 +131,20 @@ export function CRUDDialog({ title, close, queueItems, action, autoCloseOnSucces newQueue.push({ ...entry, status: entry.status === 'error' ? 'in-progress' : entry.status, - }) + }); } return newQueue; }); - for (const queueItem of queue.filter(i => i.status !== 'success')) { + for (const queueItem of queue.filter((i) => i.status !== 'success')) { try { - await action( queueItem.data, () => { updateQueueItem(queueItem, 'success'); }, - () => { - updateQueueItem(queueItem, 'error'); + (error) => { + updateQueueItem(queueItem, 'error', error); }, ); } catch (e) { @@ -134,6 +155,32 @@ export function CRUDDialog({ title, close, queueItems, action, autoCloseOnSucces setInProgress(false); }, [queue, progress]); + const mapErrors = (error: any): string => { + if (!error?.response?.status) { + return t('judo.error.technical.no-status', { defaultValue: 'Could not determine the result(status) of the operation. Please contact with the system admins.' }); + } + if (isErrorOperationFault(error)) { + if (!!faultPrefix) { + const faultObjectKeys = Object.keys(error.response.data); + const translatedErrors: string[] = []; + + for (const faultObjectKey of faultObjectKeys) { + const objectAttributes = Object.keys(error.response.data[faultObjectKey]); + for (const attribute of objectAttributes) { + translatedErrors.push(t(`faults.${faultPrefix}.${faultObjectKey}.${attribute}`, { defaultValue: attribute }) + ': ' + error.response.data[faultObjectKey][attribute]); + } + } + + return translatedErrors.join(', '); + } else { + console.error(error?.response); + return t('judo.error.unmappable', { defaultValue: 'An error occurred, but we could not display the error info.' }); + } + + } + return t('judo.error.unhandled', { defaultValue: 'An unhandled error occurred.' }); + } + useEffect(() => { if (runCount.current > 0 && !inProgress) { if (queue.every(i => i.status === 'success')) { @@ -176,13 +223,14 @@ export function CRUDDialog({ title, close, queueItems, action, autoCloseOnSucces - {queue.map(item => ( - - - {iconMapping[item.status]} - - - + {queue.map((item) => ( + + {iconMapping[item.status]} + {mapErrors(item.error)} : undefined} + /> + ))} @@ -191,10 +239,10 @@ export function CRUDDialog({ title, close, queueItems, action, autoCloseOnSucces i.status === 'success')} + disabled={queue.every((i) => i.status === 'success')} loading={inProgress} loadingPosition="start" id="use-crud-dialog-execute" @@ -216,9 +264,7 @@ function LinearProgressWithLabel(props: LinearProgressProps & { value: number }) - {`${Math.round( - props.value, - )}%`} + {`${Math.round(props.value)}%`} ); diff --git a/judo-ui-react/src/main/resources/actor/src/pages/actions/action/call-operation-action.tsx.hbs b/judo-ui-react/src/main/resources/actor/src/pages/actions/action/call-operation-action.tsx.hbs index 5f97a845..36ba96e7 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/actions/action/call-operation-action.tsx.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/actions/action/call-operation-action.tsx.hbs @@ -76,7 +76,7 @@ export type {{ actionFunctionHandlerTypeName action 'PostHandler' }} = (ownerCal export const {{ getCustomizationActionFunctionHandlerInterfaceKey action 'PostHandlerHook' }} = '{{ actionFunctionHandlerTypeName action 'PostHandlerHook' }}'; export type {{ actionFunctionHandlerTypeName action 'PostHandlerHook' }} = () => {{ actionFunctionHandlerTypeName action 'PostHandler' }}; -export type {{ actionFunctionTypeName action }} = () => ({{# if action.operation.isMapped }}owner: {{ classDataName action.dataElement.owner 'Stored' }}, {{/ if }}successCallback: () => void) => Promise; +export type {{ actionFunctionTypeName action }} = () => ({{# if action.operation.isMapped }}owner: {{ classDataName action.dataElement.owner 'Stored' }}, {{/ if }}successCallback: () => void, errorCallback?: (error: any) => void, silentMode?: boolean) => Promise; export const {{ actionFunctionHookName action }}: {{ actionFunctionTypeName action }} = () => { const { t } = useTranslation(); diff --git a/judo-ui-react/src/main/resources/actor/src/pages/actions/action/call-operation-action/without-input-form.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/pages/actions/action/call-operation-action/without-input-form.fragment.hbs index f6008c94..1486a11f 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/actions/action/call-operation-action/without-input-form.fragment.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/actions/action/call-operation-action/without-input-form.fragment.hbs @@ -1,4 +1,4 @@ -return async function {{ actionFunctionName action }} ({{# if action.operation.isMapped }}owner: {{ classDataName action.dataElement.owner 'Stored' }}, {{/ if }}successCallback: () => void) { +return async function {{ actionFunctionName action }} ({{# if action.operation.isMapped }}owner: {{ classDataName action.dataElement.owner 'Stored' }}, {{/ if }}successCallback: () => void, errorCallback?: (error: any) => void, silentMode?: boolean) { {{# if action.operation.input }} {{# with action.inputParameterPage.originalPageContainer.tables.[0] as |table| }} const columns: GridColDef<{{ classDataName action.inputParameterPage.dataElement.target 'Stored' }}>[] = [ @@ -47,11 +47,13 @@ return async function {{ actionFunctionName action }} ({{# if action.operation.i {{ else }} {{# if (hasConfirmation action) }} - // TODO: implement shiny MUI Dialog here - const result = confirm(t('{{ getTranslationKeyForAction action }}.confirmation', { defaultValue: '{{ action.confirmationMessage }}' }) as string); + if (!silentMode) { + // TODO: implement shiny MUI Dialog here + const result = confirm(t('{{ getTranslationKeyForAction action }}.confirmation', { defaultValue: '{{ action.confirmationMessage }}' }) as string); - if (!result) { - return; + if (!result) { + return; + } } {{/ if }} {{/ if }} @@ -117,48 +119,54 @@ return async function {{ actionFunctionName action }} ({{# if action.operation.i {{ else }} successCallback(); {{/ if }} - enqueueSnackbar(title, { - variant: 'success', - ...toastConfig.success, - }); + if (!silentMode) { + enqueueSnackbar(title, { + variant: 'success', + ...toastConfig.success, + }); + } } catch (error: any) { - {{# if (hasCallOperationActionFaults action) }} - if (isErrorOperationFault(error)) { - const faultObjectKeys = Object.keys(error.response.data); - const firstFaultKey = faultObjectKeys[0]; - const faultMappings: Record = { - {{# each action.operation.faults as |fault| }} - '{{ classServiceTypeName fault.target }}': (payload: {{ nameWithoutModel fault.target.name }}) => { - return { - {{# each fault.target.attributes as |attribute| }} - [t('faults.{{ classServiceTypeName fault.target }}.{{ attribute.name }}', { defaultValue: '{{ attribute.name }}' })]: payload?.{{ attribute.name }}, - {{/ each }} - }; - }, - {{/ each }} - }; + if (errorCallback) { + errorCallback(error); // consider passing mapped content here eventually + } else { + {{# if (hasCallOperationActionFaults action) }} + if (isErrorOperationFault(error)) { + const faultObjectKeys = Object.keys(error.response.data); + const firstFaultKey = faultObjectKeys[0]; + const faultMappings: Record = { + {{# each action.operation.faults as |fault| }} + '{{ classServiceTypeName fault.target }}': (payload: {{ nameWithoutModel fault.target.name }}) => { + return { + {{# each fault.target.attributes as |attribute| }} + [t('faults.{{ classServiceTypeName fault.target }}.{{ attribute.name }}', { defaultValue: '{{ attribute.name }}' })]: payload?.{{ attribute.name }}, + {{/ each }} + }; + }, + {{/ each }} + }; - createErrorDialog({ - fullWidth: true, - maxWidth: 'md', - onClose: (event: object, reason: string) => { - if (reason !== 'backdropClick') { - closeErrorDialog(); - } - }, - children: ( - closeErrorDialog()} - faultObjectKey={firstFaultKey} - content={faultMappings[firstFaultKey](error.response.data[firstFaultKey])} - /> - ), - }); - } else { + createErrorDialog({ + fullWidth: true, + maxWidth: 'md', + onClose: (event: object, reason: string) => { + if (reason !== 'backdropClick') { + closeErrorDialog(); + } + }, + children: ( + closeErrorDialog()} + faultObjectKey={firstFaultKey} + content={faultMappings[firstFaultKey](error.response.data[firstFaultKey])} + /> + ), + }); + } else { + handleActionError(error{{# if action.operation.isMapped }}, undefined, owner{{/ if }}); + } + {{ else }} handleActionError(error{{# if action.operation.isMapped }}, undefined, owner{{/ if }}); - } - {{ else }} - handleActionError(error{{# if action.operation.isMapped }}, undefined, owner{{/ if }}); - {{/ if }} + {{/ if }} + } } } diff --git a/judo-ui-react/src/main/resources/actor/src/pages/components/table/for-aggregation.hbs b/judo-ui-react/src/main/resources/actor/src/pages/components/table/for-aggregation.hbs index d7ef7fa1..4361e8ee 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/components/table/for-aggregation.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/components/table/for-aggregation.hbs @@ -155,6 +155,36 @@ export const {{ tableComponentName table }} = (props: {{ tableComponentName tabl return !!selectionModel.length && {{# if table.enabledBy }}ownerData.{{ table.enabledBy.name }}{{ else }}true{{/ if }} && isFormUpdateable() && !{{ boolValue table.dataElement.isReadOnly }}; }, [ownerData, selectionModel]); {{/ if }} + {{# each (getBulkOperationActionsForTable table) as |action| }} + const {{ actionFunctionName action }}BulkCall = async (item: {{ classDataName table.dataElement.target 'Stored' }}, successHandler: () => void, errorHandler: (error: any) => void) => { + await {{ actionFunctionName action }}(item, successHandler, errorHandler, true); + } + {{/ each }} + {{# if (tableHasBulkOperations table) }} + const bulkCallOperation = useCallback((title: string, actionName: string, action: (item: {{ classDataName table.dataElement.target 'Stored' }}, successHandler: () => void, errorHandler: (error: any) => void) => Promise) => { + openCRUDDialog<{{ classDataName table.dataElement.target 'Stored' }}>({ + dialogTitle: title, + {{# with (getFirstTitleColumnForTable table) as |column| }} + itemTitleFn: (item) => item.{{ column.attributeType.name }}!, + {{ else }} + itemTitleFn: (item) => t('judo.placeholder', { defaultValue: 'placeholder' }) as string, + {{/ with }} + selectedItems: selectedRows.current, + action: action, + onClose: (needsRefresh) => { + if (needsRefresh) { + fetchOwnerData(); + setSelectionModel([]); // not resetting on fetchData because refreshes would always remove selections... + } + }, + faultPrefix: `{{ classServiceTypeName table.dataElement.target }}.${actionName}`, + }); + }, []); + + const isBulkOperationAvailable: () => boolean = useCallback(() => { + return !!selectionModel.length; + }, [selectionModel]); + {{/ if }} {{# if (stringValueIsTrue useTableContextMenus) }} const contextMenuRef = useRef(null); @@ -370,6 +400,17 @@ export const {{ tableComponentName table }} = (props: {{ tableComponentName tabl {t('judo.pages.table.delete.selected', { defaultValue: 'Delete' })} : null} {{/ if }} + {{# each (getBulkOperationActionsForTable table) as |bulkOp| }} + {isBulkOperationAvailable() ? : null} + {{/ each }}
{/* Placeholder */}
), diff --git a/judo-ui-react/src/main/resources/actor/src/pages/components/table/for-association.hbs b/judo-ui-react/src/main/resources/actor/src/pages/components/table/for-association.hbs index 43e1b4ed..b541782b 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/components/table/for-association.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/components/table/for-association.hbs @@ -195,6 +195,31 @@ export const {{ tableComponentName table }} = forwardRef void, errorHandler: (error: any) => void) => Promise) => { + openCRUDDialog<{{ classDataName table.dataElement.target 'Stored' }}>({ + dialogTitle: title, + {{# with (getFirstTitleColumnForTable table) as |column| }} + itemTitleFn: (item) => item.{{ column.attributeType.name }}!, + {{ else }} + itemTitleFn: (item) => t('judo.placeholder', { defaultValue: 'placeholder' }) as string, + {{/ with }} + selectedItems: selectedRows.current, + action: action, + onClose: (needsRefresh) => { + if (needsRefresh) { + fetchData(); + setSelectionModel([]); // not resetting on fetchData because refreshes would always remove selections... + } + }, + faultPrefix: `{{ classServiceTypeName table.dataElement.target }}.${actionName}`, + }); + }, []); + + const isBulkOperationAvailable: () => boolean = useCallback(() => { + return !!selectionModel.length; + }, [selectionModel]); + {{/ if }} {{# if (stringValueIsTrue useTableContextMenus) }} const contextMenuRef = useRef(null); @@ -453,6 +478,17 @@ export const {{ tableComponentName table }} = forwardRef : null} {{/ if }} + {{# each (getBulkOperationActionsForTable table) as |bulkOp| }} + {isBulkOperationAvailable() ? : null} + {{/ each }}
{/* Placeholder */}
), diff --git a/judo-ui-react/src/main/resources/actor/src/pages/components/table/for-table-page.hbs b/judo-ui-react/src/main/resources/actor/src/pages/components/table/for-table-page.hbs index 397b5f4d..a773736e 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/components/table/for-table-page.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/components/table/for-table-page.hbs @@ -203,6 +203,36 @@ export const {{ tableComponentName table }} = forwardRef void, errorHandler: (error: any) => void) => { + await {{ actionFunctionName action }}(item, successHandler, errorHandler, true); + } + {{/ each }} + {{# if (tableHasBulkOperations table) }} + const bulkCallOperation = useCallback((title: string, actionName: string, action: (item: {{ classDataName table.dataElement.target 'Stored' }}, successHandler: () => void, errorHandler: (error: any) => void) => Promise) => { + openCRUDDialog<{{ classDataName table.dataElement.target 'Stored' }}>({ + dialogTitle: title, + {{# with (getFirstTitleColumnForTable table) as |column| }} + itemTitleFn: (item) => item.{{ column.attributeType.name }}!, + {{ else }} + itemTitleFn: (item) => t('judo.placeholder', { defaultValue: 'placeholder' }) as string, + {{/ with }} + selectedItems: selectedRows.current, + action: action, + onClose: (needsRefresh) => { + if (needsRefresh) { + fetchData(); + setSelectionModel([]); // not resetting on fetchData because refreshes would always remove selections... + } + }, + faultPrefix: `{{ classServiceTypeName table.dataElement.target }}.${actionName}`, + }); + }, []); + + const isBulkOperationAvailable: () => boolean = useCallback(() => { + return !!selectionModel.length; + }, [selectionModel]); + {{/ if }} {{# if (stringValueIsTrue useTableContextMenus) }} const contextMenuRef = useRef(null); @@ -418,6 +448,17 @@ export const {{ tableComponentName table }} = forwardRef : null} {{/ if }} + {{# each (getBulkOperationActionsForTable table) as |bulkOp| }} + {isBulkOperationAvailable() ? : null} + {{/ each }}
{/* Placeholder */}
), diff --git a/pom.xml b/pom.xml index 3dc6956a..8807753e 100644 --- a/pom.xml +++ b/pom.xml @@ -55,7 +55,7 @@ 18.14.2 8.5.1 - 1.1.0.20230914_115818_0769fb6c_develop + 1.1.0.20230920_125334_2462b399_develop 1.0.0.20230826_230139_c0dd2610_develop 1.0.0.20230901_223318_9282b5a8_feature_JNG_5045_inline_files