From 8db5c05632a1c0765825ea2c02c4fd7434a47059 Mon Sep 17 00:00:00 2001 From: Norbert Csaba Herczeg Date: Tue, 28 Nov 2023 18:28:20 +0100 Subject: [PATCH] JNG-4838 add post refresh hooks, and onBlur support --- docs/pages/01_ui_react.adoc | 234 +++--------------- .../ui/generator/react/UiActionsHelper.java | 45 +++- .../ui/generator/react/UiImportHelper.java | 2 +- .../judo/ui/generator/react/UiPageHelper.java | 24 ++ .../ui/generator/react/UiPandinoHelper.java | 18 ++ .../actor/src/containers/container.tsx.hbs | 6 +- .../actor/src/containers/dialog.tsx.hbs | 5 +- .../actor/src/containers/page.tsx.hbs | 4 +- .../widget-fragments/binarytypeinput.hbs | 8 +- .../containers/widget-fragments/dateinput.hbs | 4 +- .../widget-fragments/datetimeinput.hbs | 4 +- .../widget-fragments/enumerationcombo.hbs | 6 +- .../widget-fragments/enumerationradio.hbs | 4 +- .../widget-fragments/numericinput.hbs | 6 +- .../containers/widget-fragments/switch.hbs | 39 +-- .../containers/widget-fragments/textarea.hbs | 6 +- .../containers/widget-fragments/textinput.hbs | 6 +- .../containers/widget-fragments/timeinput.hbs | 6 +- .../widget-fragments/trinarylogiccombo.hbs | 4 +- .../src/custom/application-customizer.tsx.hbs | 2 +- .../resources/actor/src/dialogs/index.tsx.hbs | 27 +- .../pages/actions/RefreshAction.fragment.hbs | 3 + .../resources/actor/src/pages/index.tsx.hbs | 22 +- 23 files changed, 241 insertions(+), 244 deletions(-) diff --git a/docs/pages/01_ui_react.adoc b/docs/pages/01_ui_react.adoc index cc0a864d..d9cf7d40 100644 --- a/docs/pages/01_ui_react.adoc +++ b/docs/pages/01_ui_react.adoc @@ -434,226 +434,56 @@ const OptimisticImplementationForYayy: ComponentActorCreateYayy = ({ data, store > Of course our custom components can be placed / imported from anywhere in the source code. We just simplified it in the use-case above. -=== Implementing custom navigation logic for components +=== Overriding and extending page actions -Navigation actions are implemented as hooks. These hooks have names starting with "useRow..." in case of tables and -"useLink..." in case of single relations. +Every page has a set of Actions. These are typically actions triggered by buttons, or page lifecycle actions, and are +generated in a form of optional interface methods. -Given we have a table screen listing galaxies, we can implement a custom navigation logic in the following way: +These methods can be re-implemented one-by-one, and if the framework detects a "custom" version of a method, it will +call that instead of the original (if any). -*Generated hook (original code):* -[source,typescriptjsx] ----- -import { OBJECTCLASS } from '@pandino/pandino-api'; -import { useTrackService } from '@pandino/react-hooks'; -import type { JudoIdentifiable } from '@judo/data-api-common'; -import type { ViewGalaxyQueryCustomizer, ViewGalaxy, ViewGalaxyStored } from '../../../../../../generated/data-api'; -import { useJudoNavigation } from '../../../../../../components'; - -export const ROW_VIEW_GALAXIES_ACTION_INTERFACE_KEY = 'RowViewGalaxiesAction'; -export type RowViewGalaxiesAction = () => (entry: ViewGalaxyStored) => Promise; - -export const useRowViewGalaxiesAction: RowViewGalaxiesAction = () => { - const { navigate } = useJudoNavigation(); - const { service: useCustomNavigation } = useTrackService( - `(${OBJECTCLASS}=${ROW_VIEW_GALAXIES_ACTION_INTERFACE_KEY})`, - ); - - if (useCustomNavigation) { - const customNavigation = useCustomNavigation(); - return customNavigation; - } - - return async function (entry: ViewGalaxyStored) { - navigate(`god/galaxies/view/${entry.__signedIdentifier}`); - }; -}; ----- - -Overriding the above logic can ge done by: - -- implementing the `RowViewGalaxiesAction` interface -- registering this implementation in the `application-customizer.tsx` file - -> For brevity's sake we'll put all our code in a single file, but it's not mandatory - -*src/custom/application-customizer.tsx:* -[source,typescriptjsx] ----- -import type { BundleContext } from '@pandino/pandino-api'; -import { useJudoNavigation } from '../components'; -import { ViewGalaxyStored } from '../generated/data-api'; -import { RowViewGalaxiesAction, ROW_VIEW_GALAXIES_ACTION_INTERFACE_KEY } from '../pages/god/galaxies/table/actions'; -import { ApplicationCustomizer } from './interfaces'; - -export class DefaultApplicationCustomizer implements ApplicationCustomizer { - async customize(context: BundleContext): Promise { - context.registerService(ROW_VIEW_GALAXIES_ACTION_INTERFACE_KEY, customRowViewGalaxiesAction); - } -} - -const customRowViewGalaxiesAction: RowViewGalaxiesAction = () => { - const { navigate } = useJudoNavigation(); - - return async (entry: ViewGalaxyStored) => { - // regardless of what row we select, we will always go to the same page - navigate('god/earth/view'); - } -}; ----- - -=== Implementing custom behaviour for operation success handling - -Every custom operation has a "success handler" implementation by default. These handlers behave differently depending on -the action type, and return parameter (or lack thereof). - -*Default behaviours explained:* - -- if there is a *mapped* return type: - * pop a success toast and - * navigate to the created element's view page -- if there is an *unmapped* return type: - * pop a success toast and - * refresh the current page and - * show the result in a read-only modal -- if there is no return type: - * pop a success toast and - * refresh the current page - -*Overriding the above logic can be done by:* - -- implementing the `PostHandlerHook` interface for an operation -- registering this implementation in the `application-customizer.tsx` file - -Depending on what operation we would like to override, we need to locate the action in the `src/pages` folder, and once -we found our action file, we should be able to see an `INTERFACE_KEY` with the corresponding `PostHandlerHook` interface. - -> Please be aware that the interfaces for each hook have different signatures based on the operation, e.g.: for operations - which do not have a return type, the corresponding hook interfaces won't contain a "result" parameter! +Custom page actions can be implemented on a per-page basis. Every page as a designated unique `INTERFACE_KEY` string and +a corresponding action hook `type`. -*src/custom/application-customizer.tsx:* -[source,typescriptjsx] ----- -import type { BundleContext } from '@pandino/pandino-api'; -import { useSnackbar } from 'notistack'; -import { ApplicationCustomizer } from './interfaces'; -import { - ADMIN_DASHBOARD_CREATE_ISSUE_ACTION_POST_HANDLER_HOOK_INTERFACE_KEY, - AdminDashboardCreateIssueActionPostHandlerHook -} from '../pages/admin/admin/dashboardhome/actions'; -import { AdminIssueStored } from '../generated/data-api'; -import { toastConfig } from '../config'; +*Figuring out how to locate interface keys can be done via:* -export class DefaultApplicationCustomizer implements ApplicationCustomizer { - async customize(context: BundleContext): Promise { - context.registerService(ADMIN_DASHBOARD_CREATE_ISSUE_ACTION_POST_HANDLER_HOOK_INTERFACE_KEY, usePostIssueCreated); - } -} +- observing the page route in the browsers URL bar (for non-dialogs), and looking the up in the `src/routes.tsx` file +- Inspecting the pages / dialogs in dev-tools, and searching for the id of them in the code -const usePostIssueCreated: AdminDashboardCreateIssueActionPostHandlerHook = () => { - const { enqueueSnackbar } = useSnackbar(); - // We can add any variables and use any hooks here - - return async (ownerCallback: () => void, result?: AdminIssueStored) => { - // The default implementation in this case is to pop a generic toast, and navigate to the created element's page - // but we are overriding this, to only pop a custom toast message - if (result) { - enqueueSnackbar(`${result.title} created!`, { - variant: 'success', - ...toastConfig.success, - }); - } - - // and regardless of the output, refresh the current page - ownerCallback(); - }; -}; ----- - -Since operations are always started from modal windows the `ownerCallback` can behave differently, but most of the time -it triggers a refresh on the actual page which we resided on. - -If we would like to implement a fix navigation to some page, it is recommended to *NOT CALL* the `ownerCallback()` function -to prevent unnecessary REST calls and potential screen flow issues. - -=== Implementing a post refresh hook +*Registering implementations* -Whenever pages have a `refresh` action, we have the option to register a hook in which we can "intercept" the -"after/post" lifecycle for them. - -These hooks are registered with a specific `INTERFACE_KEY`. We can locate these keys in the `src/pages` folder, and once -we found our page/dialog file, we should be able to see an `INTERFACE_KEY` with the corresponding `PostRefreshHook` interface. +Implementations can be registered in one central location: `src/custom/application-customizer.tsx`. *src/custom/application-customizer.tsx:* [source,typescriptjsx] ---- import type { BundleContext } from '@pandino/pandino-api'; -import type { Dispatch, SetStateAction } from 'react'; -import { ApplicationCustomizer } from './interfaces'; -import { GOD_GALAXIES_VIEW_POST_REFRESH_HOOK_INTERFACE_KEY, GodGalaxiesViewPostRefreshHook } from '~/pages/god/galaxies/view'; -import { ViewGalaxy, ViewGalaxyStored } from '~/generated/data-api'; - -export class DefaultApplicationCustomizer implements ApplicationCustomizer { - async customize(context: BundleContext): Promise { - // register your implementations here - context.registerService(GOD_GALAXIES_VIEW_POST_REFRESH_HOOK_INTERFACE_KEY, customGodGalaxiesViewRefreshPostHandlerHook); - } -} - -const customGodGalaxiesViewRefreshPostHandlerHook: GodGalaxiesViewPostRefreshHook = () => { - return async (data: ViewGalaxyStored, - storeDiff: (attributeName: keyof ViewGalaxyStored, value: any) => void, - setEditMode: Dispatch>, - setValidation: Dispatch>>, - ) => { - if (data.name === 'Acallaris') { - setValidation((validation) => { - validation.set('constellation', 'Wrong constellation, lmao'); - return validation; - }); - } - }; -}; ----- - -=== Implementing an onBlur hook - -OnBlur hooks are available for all form elements which have the flag enabled in the model. - -Just like every other hook, these have their corresponding `INTERFACE_KEY` and the interfaces for the keys. - -*src/custom/application-customizer.tsx:* -[source,typescriptjsx] ----- -import type { Dispatch, SetStateAction } from 'react'; -import type { BundleContext } from '@pandino/pandino-api'; -import { ApplicationCustomizer } from './interfaces'; -import { GOD_CREATE_NAME_ON_BLUR_INTERFACE_KEY, GodCreateNameOnBlurHook } from '~/pages/god/galaxies/table/actions/PageCreateGalaxiesForm'; -import type { ViewGalaxy } from '~/generated/data-api'; - +import type { ApplicationCustomizer } from './interfaces'; +import type { ViewGalaxyViewActionsHook } from '~/pages/God/Galaxies/AccessViewPage'; +import { GOD_GALAXIES_ACCESS_VIEW_PAGE_ACTIONS_HOOK_INTERFACE_KEY } from '~/pages/God/Galaxies/AccessViewPage'; export class DefaultApplicationCustomizer implements ApplicationCustomizer { async customize(context: BundleContext): Promise { - // register your implementations here - - context.registerService(GOD_CREATE_NAME_ON_BLUR_INTERFACE_KEY, customGodCreateNameOnBlurHook); + context.registerService(GOD_GALAXIES_ACCESS_VIEW_PAGE_ACTIONS_HOOK_INTERFACE_KEY, customViewGalaxyViewActionsHook); } } -const customGodCreateNameOnBlurHook: GodCreateNameOnBlurHook = () => { - return async ( - data: ViewGalaxy, - storeDiff: (attributeName: keyof ViewGalaxy, value: any) => void, - editMode: boolean, - setEditMode: Dispatch>, - setValidation: Dispatch>>, - submit: () => Promise, - ) => { - if (data.name === 'test') { - storeDiff('constellation', data.name); - storeDiff('nakedEye', true); - await submit(); - } +const customViewGalaxyViewActionsHook: ViewGalaxyViewActionsHook = () => { + return { + onNakedEyeBlurAction: async (data, storeDiff, editMode, setValidation, submit) => { + // If the are toggling the `nakedEye` property and not in editMode already, then automatically save the change + if (!editMode) { + await submit(); + } + }, + postRefreshAction: async (data , storeDiff, setValidation) => { + // Check the `nakedEye` property after every refresh, and if it is not filled, then set a validation message. + if (!data.nakedEye) { + setValidation(new Map([ + ['nakedEye', 'Naked Eye has to be checked!'] + ])); + } + }, }; }; ---- diff --git a/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiActionsHelper.java b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiActionsHelper.java index b94fa1ce..7641ffa8 100644 --- a/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiActionsHelper.java +++ b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiActionsHelper.java @@ -32,8 +32,7 @@ import java.util.List; import java.util.stream.Collectors; -import static hu.blackbelt.judo.ui.generator.react.UiPageHelper.isSingleAccessPage; -import static hu.blackbelt.judo.ui.generator.react.UiPageHelper.pageHasSignedId; +import static hu.blackbelt.judo.ui.generator.react.UiPageHelper.*; import static hu.blackbelt.judo.ui.generator.react.UiWidgetHelper.collectElementsOfType; import static hu.blackbelt.judo.ui.generator.react.UiWidgetHelper.getReferenceClassType; import static hu.blackbelt.judo.ui.generator.typescript.rest.commons.UiCommonsHelper.classDataName; @@ -284,4 +283,46 @@ public static String refreshActionDataParameter(Action action) { } return "undefined"; } + + public static String postCallOperationActionParams(PageDefinition page, ActionDefinition actionDefinition) { + List tokens = new ArrayList<>(); + if (actionDefinition.getTargetType() != null) { + tokens.add("data: " + classDataName(actionDefinition.getTargetType(), "Stored")); + } + if (actionDefinition instanceof CallOperationActionDefinition call && call.getOperation().getOutput() != null) { + tokens.add("output: " + classDataName(call.getOperation().getOutput().getTarget(), "Stored")); + } + if (page.getContainer().isForm()) { + String result = (pageHasOutputTarget(page) ? classDataName(getPageOutputTarget(page), "Stored") : dialogDataType(page)) + (page.getContainer().isTable() ? "[]" : ""); + tokens.add("onSubmit: (result?: " + result + ") => Promise"); + } + if (page.isOpenInDialog()) { + tokens.add("onClose: () => Promise"); + } + return String.join(", ", tokens); + } + + public static String postRefreshActionParams(PageDefinition page, ActionDefinition actionDefinition) { + String res = ""; + res += "data: " + classDataName(getReferenceClassType(page), "Stored") + (page.getContainer().isTable() ? "[]" : ""); + if (!page.getContainer().isTable()) { + res += ", "; + res += "storeDiff: (attributeName: keyof " + classDataName(getReferenceClassType(page), "") + ", value: any) => void, "; + res += "setValidation: Dispatch>>"; + } + return res; + } + + public static String onBlurActionParams(PageContainer container) { + List tokens = new ArrayList<>(); + if (!container.isTable()) { + tokens.add("data: " + classDataName((ClassType) container.getDataElement(), "Stored")); + tokens.add("storeDiff: (attributeName: keyof " + classDataName((ClassType) container.getDataElement(), "") + ", value: any) => void"); + tokens.add("editMode: boolean"); + tokens.add("setValidation: Dispatch>>"); + } + tokens.add("submit: () => Promise"); + + return String.join(", ", tokens); + } } diff --git a/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiImportHelper.java b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiImportHelper.java index fb7eacd2..b22a47f0 100644 --- a/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiImportHelper.java +++ b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiImportHelper.java @@ -50,7 +50,7 @@ public class UiImportHelper { Map.entry("link", Set.of()), Map.entry("numericinput", Set.of("TextField", "InputAdornment")), Map.entry("spacer", Set.of()), - Map.entry("switch", Set.of("TextField", "MenuItem", "InputAdornment", "FormGroup", "FormControlLabel", "Checkbox")), + Map.entry("switch", Set.of("TextField", "MenuItem", "InputAdornment", "FormGroup", "FormControlLabel", "Checkbox", "FormControl", "FormHelperText")), Map.entry("tabcontroller", Set.of()), Map.entry("table", Set.of("Button")), Map.entry("text", Set.of("Typography")), diff --git a/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiPageHelper.java b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiPageHelper.java index ed31d2ee..8a45dab4 100644 --- a/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiPageHelper.java +++ b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiPageHelper.java @@ -359,4 +359,28 @@ public static String dialogDataType(PageDefinition page) { } return "void"; } + + public static Action getCreateActionForPage(PageDefinition page) { + ActionDefinition def = page.getContainer().getActionButtonGroup().getButtons().stream().map(Button::getActionDefinition).filter(a -> a instanceof CreateActionDefinition).findFirst().orElse(null); + if (def != null) { + return page.getActions().stream().filter(a -> a.getActionDefinition().equals(def)).findFirst().orElse(null); + } + return null; + } + + public static Action getUpdateActionForPage(PageDefinition page) { + ActionDefinition def = page.getContainer().getActionButtonGroup().getButtons().stream().map(Button::getActionDefinition).filter(a -> a instanceof UpdateActionDefinition).findFirst().orElse(null); + if (def != null) { + return page.getActions().stream().filter(a -> a.getActionDefinition().equals(def)).findFirst().orElse(null); + } + return null; + } + + public static Action getCallOperationActionForPage(PageDefinition page) { + ActionDefinition def = page.getContainer().getActionButtonGroup().getButtons().stream().map(Button::getActionDefinition).filter(a -> a instanceof CallOperationActionDefinition).findFirst().orElse(null); + if (def != null) { + return page.getActions().stream().filter(a -> a.getActionDefinition().equals(def)).findFirst().orElse(null); + } + return null; + } } diff --git a/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiPandinoHelper.java b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiPandinoHelper.java index 6cd594e0..64f6d6b4 100644 --- a/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiPandinoHelper.java +++ b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiPandinoHelper.java @@ -22,9 +22,11 @@ import hu.blackbelt.judo.generator.commons.annotations.TemplateHelper; import hu.blackbelt.judo.meta.ui.*; +import hu.blackbelt.judo.meta.ui.data.AttributeType; import lombok.extern.java.Log; import java.util.*; +import java.util.stream.Collectors; import static hu.blackbelt.judo.ui.generator.react.UiPageContainerHelper.containerComponentName; import static hu.blackbelt.judo.ui.generator.react.UiPageContainerHelper.simpleActionDefinitionName; @@ -86,4 +88,20 @@ public static List getAllCallOperationActions(PageDefinition pageDefinit .sorted(Comparator.comparing(NamedElement::getFQName)) .toList(); } + + public static List getOnBlurAttributesForContainer(PageContainer container) { + Set elements = new LinkedHashSet<>(); + collectVisualElementsMatchingCondition(container, e -> e.getOnBlur() != null && e.getOnBlur(), elements); + + Set filtered = new LinkedHashSet<>(); + + for (VisualElement e: elements) { + AttributeType attributeType = ((Input) e).getAttributeType(); + if (filtered.stream().noneMatch(a -> a.getName().equals(attributeType.getName()))) { + filtered.add(attributeType); + } + } + + return filtered.stream().sorted(Comparator.comparing(NamedElement::getFQName)).collect(Collectors.toList()); + } } diff --git a/judo-ui-react/src/main/resources/actor/src/containers/container.tsx.hbs b/judo-ui-react/src/main/resources/actor/src/containers/container.tsx.hbs index 025e590d..405f4f45 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/container.tsx.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/container.tsx.hbs @@ -30,6 +30,9 @@ export interface {{ pageContainerActionDefinitionTypeName container }}{{# if (co {{# each (getContainerOwnActionDefinitions container) as |actionDefinition| }} {{ simpleActionDefinitionName actionDefinition }}?: ({{{ getContainerOwnActionParameters actionDefinition container }}}) => Promise<{{ getContainerOwnActionReturnType actionDefinition container }}>; {{/ each }} + {{# each (getOnBlurAttributesForContainer container) as |attributeType| }} + on{{ firstToUpper attributeType.name }}BlurAction?: ({{{ onBlurActionParams container }}}) => void; + {{/ each }} } {{/ unless }} @@ -51,6 +54,7 @@ export interface {{ containerComponentName container }}Props { editMode: boolean; validation: Map; setValidation: Dispatch>>; + submit: () => Promise; {{/ unless }} {{/ unless }} }; @@ -61,7 +65,7 @@ export default function {{ containerComponentName container }}(props: {{ contain {{# unless (containerIsEmptyDashboard container) }} const { t } = useTranslation(); const { navigate, back } = useJudoNavigation(); - const { refreshCounter, actions{{# if container.isSelector }}, selectionDiff, setSelectionDiff{{/ if }}{{# if container.isRelationSelector }}, alreadySelected{{/ if }}{{# unless container.table }}, data, isLoading, isFormUpdateable, isFormDeleteable, storeDiff, editMode, validation, setValidation{{/ unless }} } = props; + const { refreshCounter, actions{{# if container.isSelector }}, selectionDiff, setSelectionDiff{{/ if }}{{# if container.isRelationSelector }}, alreadySelected{{/ if }}{{# unless container.table }}, data, isLoading, isFormUpdateable, isFormDeleteable, storeDiff, editMode, validation, setValidation, submit{{/ unless }} } = props; const { locale: l10nLocale } = useL10N(); {{# unless container.table }} diff --git a/judo-ui-react/src/main/resources/actor/src/containers/dialog.tsx.hbs b/judo-ui-react/src/main/resources/actor/src/containers/dialog.tsx.hbs index da07dc1b..f3a00ccb 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/dialog.tsx.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/dialog.tsx.hbs @@ -55,6 +55,7 @@ export interface {{ containerComponentName container }}DialogProps { storeDiff: (attributeName: keyof {{ classDataName container.dataElement '' }}, value: any) => void; validation: Map; setValidation: Dispatch>>; + submit: () => Promise; {{/ unless }} {{/ unless }} }; @@ -88,7 +89,8 @@ export default function {{ containerComponentName container }}Dialog(props: {{ c isFormDeleteable, storeDiff, validation, - setValidation + setValidation, + submit {{/ unless }} {{/ unless }} } = props; @@ -139,6 +141,7 @@ export default function {{ containerComponentName container }}Dialog(props: {{ c isFormDeleteable={isFormDeleteable} validation={validation} setValidation={setValidation} + submit={submit} {{/ unless }} /> diff --git a/judo-ui-react/src/main/resources/actor/src/containers/page.tsx.hbs b/judo-ui-react/src/main/resources/actor/src/containers/page.tsx.hbs index 749193e3..6a10ebbf 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/page.tsx.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/page.tsx.hbs @@ -50,6 +50,7 @@ export interface {{ containerComponentName container }}PageProps { storeDiff: (attributeName: keyof {{ classDataName container.dataElement '' }}, value: any) => void; validation: Map; setValidation: Dispatch>>; + submit: () => Promise; {{/ unless }} {{/ unless }} }; @@ -61,7 +62,7 @@ export default function {{ containerComponentName container }}Page (props: {{ co {{# unless (containerIsEmptyDashboard container) }} const { t } = useTranslation(); const { navigate, back } = useJudoNavigation(); - const { title, actions, isLoading, editMode, refreshCounter{{# unless container.table }}, data, isFormUpdateable, isFormDeleteable, storeDiff, validation, setValidation{{/ unless }} } = props; + const { title, actions, isLoading, editMode, refreshCounter{{# unless container.table }}, data, isFormUpdateable, isFormDeleteable, storeDiff, validation, setValidation, submit{{/ unless }} } = props; {{# unless container.table }} const queryCustomizer: {{ classDataName container.dataElement 'QueryCustomizer' }} = { _mask: '{{ getMaskForView container }}', @@ -126,6 +127,7 @@ export default function {{ containerComponentName container }}Page (props: {{ co isFormDeleteable={isFormDeleteable} validation={validation} setValidation={setValidation} + submit={submit} {{/ unless }} {{/ unless }} /> diff --git a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/binarytypeinput.hbs b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/binarytypeinput.hbs index af275ae0..4d055c6d 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/binarytypeinput.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/binarytypeinput.hbs @@ -33,16 +33,20 @@ deleteCallback={ async () => { storeDiff('{{ child.attributeType.name }}', null); {{# if child.onBlur }} + if (actions?.on{{ firstToUpper child.attributeType.name }}BlurAction) { const eagerCopy = { ...data, ['{{ child.attributeType.name }}']: null }; - {{ child.name }}OnBlurAction && {{ child.name }}OnBlurAction(eagerCopy, storeDiff, editMode, setEditMode, setValidation, async () => {await submit();}); + actions.on{{ firstToUpper child.attributeType.name }}BlurAction(eagerCopy, storeDiff, editMode, setValidation, submit); + } {{/ if }} } } {{/unless}} uploadCallback={ async (uploadedData: { token: string }) => { storeDiff('{{ child.attributeType.name }}', uploadedData.token); {{# if child.onBlur }} + if (actions?.on{{ firstToUpper child.attributeType.name }}BlurAction) { const eagerCopy = { ...data, ['{{ child.attributeType.name }}']: uploadedData.token }; - {{ child.name }}OnBlurAction && {{ child.name }}OnBlurAction(eagerCopy, storeDiff, editMode, setEditMode, setValidation, async () => {await submit();}); + actions.on{{ firstToUpper child.attributeType.name }}BlurAction(eagerCopy, storeDiff, editMode, setValidation, submit); + } {{/ if }} } } /> diff --git a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/dateinput.hbs b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/dateinput.hbs index 22c26044..e8a49e6a 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/dateinput.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/dateinput.hbs @@ -52,8 +52,10 @@ onChange={ (newValue?: any | null) => { storeDiff('{{ child.attributeType.name }}', newValue); {{# if child.onBlur }} + if (actions?.on{{ firstToUpper child.attributeType.name }}BlurAction) { const eagerCopy = { ...data, ['{{ child.attributeType.name }}']: newValue }; - {{ child.name }}OnBlurAction && {{ child.name }}OnBlurAction(eagerCopy, storeDiff, editMode, setEditMode, setValidation, async () => {await submit();}); + actions.on{{ firstToUpper child.attributeType.name }}BlurAction(eagerCopy, storeDiff, editMode, setValidation, submit); + } {{/ if }} } } /> diff --git a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/datetimeinput.hbs b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/datetimeinput.hbs index 214cf494..215223bb 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/datetimeinput.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/datetimeinput.hbs @@ -54,8 +54,10 @@ onChange={ (newValue: Date) => { storeDiff('{{ child.attributeType.name }}', newValue); {{# if child.onBlur }} + if (actions?.on{{ firstToUpper child.attributeType.name }}BlurAction) { const eagerCopy = { ...data, ['{{ child.attributeType.name }}']: newValue }; - {{ child.name }}OnBlurAction && {{ child.name }}OnBlurAction(eagerCopy, storeDiff, editMode, setEditMode, setValidation, async () => {await submit();}); + actions.on{{ firstToUpper child.attributeType.name }}BlurAction(eagerCopy, storeDiff, editMode, setValidation, submit); + } {{/ if }} } } /> diff --git a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/enumerationcombo.hbs b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/enumerationcombo.hbs index 64c532bb..980080ad 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/enumerationcombo.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/enumerationcombo.hbs @@ -26,7 +26,11 @@ error={ !!validation.get('{{ child.attributeType.name }}') } helperText={ validation.get('{{ child.attributeType.name }}') } {{# if child.onBlur }} - onBlur={ () => {{ child.name }}OnBlurAction && {{ child.name }}OnBlurAction(data, storeDiff, editMode, setEditMode, setValidation, async () => {await submit();}) } + onBlur={ () => { + if (actions?.on{{ firstToUpper child.attributeType.name }}BlurAction) { + actions.on{{ firstToUpper child.attributeType.name }}BlurAction(data, storeDiff, editMode, setValidation, submit); + } + } } {{/ if }} onChange={ (event) => { storeDiff('{{ child.attributeType.name }}', event.target.value); diff --git a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/enumerationradio.hbs b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/enumerationradio.hbs index fc1faf5e..042b65aa 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/enumerationradio.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/enumerationradio.hbs @@ -30,8 +30,10 @@ onChange={ (event) => { storeDiff('{{ child.attributeType.name }}', event.target.value); {{# if child.onBlur }} + if (actions?.on{{ firstToUpper child.attributeType.name }}BlurAction) { const eagerCopy = { ...data, ['{{ child.attributeType.name }}']: event.target.value }; - {{ child.name }}OnBlurAction && {{ child.name }}OnBlurAction(eagerCopy, storeDiff, editMode, setEditMode, setValidation, async () => {await submit();}); + actions.on{{ firstToUpper child.attributeType.name }}BlurAction(eagerCopy, storeDiff, editMode, setValidation, submit); + } {{/ if }} } } > diff --git a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/numericinput.hbs b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/numericinput.hbs index 3393abd8..79f34e0b 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/numericinput.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/numericinput.hbs @@ -30,7 +30,11 @@ error={ !!validation.get('{{ child.attributeType.name }}') } helperText={ validation.get('{{ child.attributeType.name }}') } {{# if child.onBlur }} - onBlur={ () => {{ child.name }}OnBlurAction && {{ child.name }}OnBlurAction(data, storeDiff, editMode, setEditMode, setValidation, async () => {await submit();}) } + onBlur={ () => { + if (actions?.on{{ firstToUpper child.attributeType.name }}BlurAction) { + actions.on{{ firstToUpper child.attributeType.name }}BlurAction(data, storeDiff, editMode, setValidation, submit); + } + } } {{/ if }} onValueChange={(values, sourceInfo) => { const newValue = values.floatValue === undefined ? null : values.floatValue; diff --git a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/switch.hbs b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/switch.hbs index 0176c901..83553a39 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/switch.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/switch.hbs @@ -9,23 +9,28 @@ storeDiff={storeDiff} > {{/ if }} - - { - storeDiff('{{ child.attributeType.name }}', event.target.checked); - {{# if child.onBlur }} - const eagerCopy = { ...data, ['{{ child.attributeType.name }}']: event.target.checked }; - {{ child.name }}OnBlurAction && {{ child.name }}OnBlurAction(eagerCopy, storeDiff, editMode, setEditMode, setValidation, async () => {await submit();}); - {{/ if }} - } } /> - } - label={ t('{{ getTranslationKeyForVisualElement child }}', { defaultValue: '{{ child.label }}' }) as string } - /> - + + + validation.has('{{ child.attributeType.name }}') ? theme.palette.error.main : 'primary' } } + disabled={ {{# if child.enabledBy }}!data.{{ child.enabledBy.name }} ||{{/ if }} {{ boolValue child.attributeType.isReadOnly }} || !isFormUpdateable() || isLoading } + control={ + validation.has('{{ child.attributeType.name }}') ? theme.palette.error.main : 'primary' } } onChange={ (event) => { + storeDiff('{{ child.attributeType.name }}', event.target.checked); + {{# if child.onBlur }} + if (actions?.on{{ firstToUpper child.attributeType.name }}BlurAction) { + const eagerCopy = { ...data, ['{{ child.attributeType.name }}']: event.target.checked }; + actions.on{{ firstToUpper child.attributeType.name }}BlurAction(eagerCopy, storeDiff, editMode, setValidation, submit); + } + {{/ if }} + } } /> + } + label={ t('{{ getTranslationKeyForVisualElement child }}', { defaultValue: '{{ child.label }}' }) as string } + /> + + {validation.has('{{ child.attributeType.name }}') && {validation.get('{{ child.attributeType.name }}')}} + {{# if child.customImplementation }} {{/ if }} diff --git a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/textarea.hbs b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/textarea.hbs index 106720b3..7fa146c9 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/textarea.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/textarea.hbs @@ -28,7 +28,11 @@ error={ !!validation.get('{{ child.attributeType.name }}') } helperText={ validation.get('{{ child.attributeType.name }}') } {{# if child.onBlur }} - onBlur={ () => {{ child.name }}OnBlurAction && {{ child.name }}OnBlurAction(data, storeDiff, editMode, setEditMode, setValidation, async () => {await submit();}) } + onBlur={ () => { + if (actions?.on{{ firstToUpper child.attributeType.name }}BlurAction) { + actions.on{{ firstToUpper child.attributeType.name }}BlurAction(data, storeDiff, editMode, setValidation, submit); + } + } } {{/ if }} onChange={ (event) => { const realValue = event.target.value?.length === 0 ? null : event.target.value; diff --git a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/textinput.hbs b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/textinput.hbs index 78a4dce5..81d6d33c 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/textinput.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/textinput.hbs @@ -26,7 +26,11 @@ error={ !!validation.get('{{ child.attributeType.name }}') } helperText={ validation.get('{{ child.attributeType.name }}') } {{# if child.onBlur }} - onBlur={ () => {{ child.name }}OnBlurAction && {{ child.name }}OnBlurAction(data, storeDiff, editMode, setEditMode, setValidation, async () => {await submit();}) } + onBlur={ () => { + if (actions?.on{{ firstToUpper child.attributeType.name }}BlurAction) { + actions.on{{ firstToUpper child.attributeType.name }}BlurAction(data, storeDiff, editMode, setValidation, submit); + } + } } {{/ if }} onChange={ (event) => { const realValue = event.target.value?.length === 0 ? null : event.target.value; diff --git a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/timeinput.hbs b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/timeinput.hbs index ac64fce5..000fde9e 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/timeinput.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/timeinput.hbs @@ -52,7 +52,11 @@ readOnly={ {{ boolValue child.attributeType.isReadOnly }} || !isFormUpdateable() } disabled={ {{# if child.enabledBy }}!data.{{ child.enabledBy.name }} ||{{/ if }} isLoading } {{# if child.onBlur }} - onBlur={ () => {{ child.name }}OnBlurAction && {{ child.name }}OnBlurAction(data, storeDiff, editMode, setEditMode, setValidation, async () => {await submit();}) } + onBlur={ () => { + if (actions?.on{{ firstToUpper child.attributeType.name }}BlurAction) { + actions.on{{ firstToUpper child.attributeType.name }}BlurAction(data, storeDiff, editMode, setValidation, submit); + } + } } {{/ if }} onChange={ (newValue: string | null | undefined) => { storeDiff('{{ child.attributeType.name }}', newValue); diff --git a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/trinarylogiccombo.hbs b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/trinarylogiccombo.hbs index 95849ee3..b610b65c 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/trinarylogiccombo.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/widget-fragments/trinarylogiccombo.hbs @@ -24,8 +24,10 @@ onChange={ (value) => { storeDiff('{{ child.attributeType.name }}', value); {{# if child.onBlur }} + if (actions?.on{{ firstToUpper child.attributeType.name }}BlurAction) { const eagerCopy = { ...data, ['{{ child.attributeType.name }}']: value }; - {{ child.name }}OnBlurAction && {{ child.name }}OnBlurAction(eagerCopy, storeDiff, editMode, setEditMode, setValidation, async () => {await submit();}); + actions.on{{ firstToUpper child.attributeType.name }}BlurAction(eagerCopy, storeDiff, editMode, setValidation, submit); + } {{/ if }} } } disabled={ {{# if child.enabledBy }}!data.{{ child.enabledBy.name }} ||{{/ if }} {{ boolValue child.attributeType.isReadOnly }} || isLoading } diff --git a/judo-ui-react/src/main/resources/actor/src/custom/application-customizer.tsx.hbs b/judo-ui-react/src/main/resources/actor/src/custom/application-customizer.tsx.hbs index 6240509c..50bf7d79 100644 --- a/judo-ui-react/src/main/resources/actor/src/custom/application-customizer.tsx.hbs +++ b/judo-ui-react/src/main/resources/actor/src/custom/application-customizer.tsx.hbs @@ -1,7 +1,7 @@ {{> fragment.header.hbs }} import type { BundleContext } from '@pandino/pandino-api'; -import { ApplicationCustomizer } from './interfaces'; +import type { ApplicationCustomizer } from './interfaces'; export class DefaultApplicationCustomizer implements ApplicationCustomizer { async customize(context: BundleContext): Promise { diff --git a/judo-ui-react/src/main/resources/actor/src/dialogs/index.tsx.hbs b/judo-ui-react/src/main/resources/actor/src/dialogs/index.tsx.hbs index 02356d1e..f65767d8 100644 --- a/judo-ui-react/src/main/resources/actor/src/dialogs/index.tsx.hbs +++ b/judo-ui-react/src/main/resources/actor/src/dialogs/index.tsx.hbs @@ -2,6 +2,9 @@ import { {{# unless page.container.table }}useCallback, useEffect, useRef, {{/ unless }}useState, lazy, Suspense } from 'react'; {{# unless (containerIsEmptyDashboard page.container) }} + {{# unless page.container.table }} + import type { Dispatch, SetStateAction } from 'react'; + {{/ unless }} import { OBJECTCLASS } from '@pandino/pandino-api'; import { useTrackService } from '@pandino/react-hooks'; import type { JudoIdentifiable } from '@judo/data-api-common'; @@ -46,13 +49,11 @@ import { {{# unless page.container.table }}useCallback, useEffect, useRef, {{/ u {{# unless (containerIsEmptyDashboard page.container) }} export type {{ containerComponentName page.container }}DialogActionsExtended = {{ containerComponentName page.container }}DialogActions & { {{# each (getAllCallOperationActions page) as |action| }} - post{{ firstToUpper (simpleActionDefinitionName action.actionDefinition) }}?: ( - {{# if action.actionDefinition.targetType }}target: {{ classDataName action.actionDefinition.targetType 'Stored' }},{{/ if }} - {{# if action.actionDefinition.operation.output }}output: {{ classDataName action.actionDefinition.operation.output.target '' }},{{/ if }} - {{# if page.container.form }}onSubmit: (result?: {{# if (pageHasOutputTarget page) }}{{ classDataName (getPageOutputTarget page) 'Stored' }}{{ else }}{{ dialogDataType page }}{{# if page.container.table }}[]{{/ if }}{{/ if }}) => Promise,{{/ if }} - onClose: () => Promise - ) => Promise; + post{{ firstToUpper (simpleActionDefinitionName action.actionDefinition) }}?: ({{{ postCallOperationActionParams page action.actionDefinition }}}) => Promise; {{/ each }} + {{# with (getRefreshActionDefinitionForContainer page.container) as |refreshActionDefinition| }} + post{{ firstToUpper (simpleActionDefinitionName refreshActionDefinition) }}?: ({{{ postRefreshActionParams page refreshActionDefinition }}}) => Promise; + {{/ with }} }; export const {{ camelCaseNameToInterfaceKey (pageName page) }}_ACTIONS_HOOK_INTERFACE_KEY = '{{ containerComponentName page.container }}ActionsHook'; @@ -208,6 +209,19 @@ export default function {{ pageName page }}(props: {{ pageName page }}Props) { const title: string = t('{{ getTranslationKeyForVisualElement page.container }}', { defaultValue: '{{ page.container.label }}' }); {{/ if }} + // Private actions + const submit = async () => { + {{# with (getCreateActionForPage page) as |action| }} + await {{ simpleActionDefinitionName action.actionDefinition }}(); + {{/ with }} + {{# with (getUpdateActionForPage page) as |action| }} + await {{ simpleActionDefinitionName action.actionDefinition }}(); + {{/ with }} + {{# with (getCallOperationActionForPage page) as |action| }} + await {{ simpleActionDefinitionName action.actionDefinition }}(); + {{/ with }} + }; + // Action section {{# each page.actions as |action| }} {{> (getActionTemplate action) }} @@ -255,6 +269,7 @@ export default function {{ pageName page }}(props: {{ pageName page }}Props) { isFormDeleteable={isFormDeleteable} validation={validation} setValidation={setValidation} + submit={submit} {{/ unless }} {{/ unless }} /> diff --git a/judo-ui-react/src/main/resources/actor/src/pages/actions/RefreshAction.fragment.hbs b/judo-ui-react/src/main/resources/actor/src/pages/actions/RefreshAction.fragment.hbs index f1f55958..4aec21cf 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/actions/RefreshAction.fragment.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/actions/RefreshAction.fragment.hbs @@ -19,6 +19,9 @@ const {{ simpleActionDefinitionName action.actionDefinition }} = async (queryCus __version: result.__version, __entityType: result.__entityType, } as Record; + if (customActions?.post{{ firstToUpper (simpleActionDefinitionName action.actionDefinition) }}) { + await customActions?.post{{ firstToUpper (simpleActionDefinitionName action.actionDefinition) }}(result{{# unless page.container.table }}, storeDiff, setValidation{{/ unless }}); + } return result; {{/ if }} } catch (error) { diff --git a/judo-ui-react/src/main/resources/actor/src/pages/index.tsx.hbs b/judo-ui-react/src/main/resources/actor/src/pages/index.tsx.hbs index affd6e25..3a5f867c 100644 --- a/judo-ui-react/src/main/resources/actor/src/pages/index.tsx.hbs +++ b/judo-ui-react/src/main/resources/actor/src/pages/index.tsx.hbs @@ -2,6 +2,9 @@ import { {{# unless page.container.table }}useCallback, useEffect, useRef, {{/ unless }}useState, lazy, Suspense } from 'react'; {{# unless (containerIsEmptyDashboard page.container) }} + {{# unless page.container.table }} + import type { Dispatch, SetStateAction } from 'react'; + {{/ unless }} import { OBJECTCLASS } from '@pandino/pandino-api'; import { useTrackService } from '@pandino/react-hooks'; import type { JudoIdentifiable } from '@judo/data-api-common'; @@ -47,8 +50,11 @@ import { {{# unless page.container.table }}useCallback, useEffect, useRef, {{/ u {{# unless (containerIsEmptyDashboard page.container) }} export type {{ containerComponentName page.container }}PageActionsExtended = {{ containerComponentName page.container }}PageActions & { {{# each (getAllCallOperationActions page) as |action| }} - post{{ firstToUpper (simpleActionDefinitionName action.actionDefinition) }}?: ({{# if action.actionDefinition.targetType }}target: {{ classDataName action.actionDefinition.targetType 'Stored' }}{{/ if }}{{# if action.actionDefinition.operation.output }}{{# if action.actionDefinition.targetType }},{{/ if }}output: {{ classDataName action.actionDefinition.operation.output.target '' }}{{/ if }}) => Promise; + post{{ firstToUpper (simpleActionDefinitionName action.actionDefinition) }}?: ({{{ postCallOperationActionParams page action.actionDefinition }}}) => Promise; {{/ each }} + {{# with (getRefreshActionDefinitionForContainer page.container) as |refreshActionDefinition| }} + post{{ firstToUpper (simpleActionDefinitionName refreshActionDefinition) }}?: ({{{ postRefreshActionParams page refreshActionDefinition }}}) => Promise; + {{/ with }} }; export const {{ camelCaseNameToInterfaceKey (pageName page) }}_ACTIONS_HOOK_INTERFACE_KEY = '{{ containerComponentName page.container }}ActionsHook'; @@ -147,6 +153,19 @@ export default function {{ pageName page }}() { const title: string = t('{{ getTranslationKeyForVisualElement page.container }}', { defaultValue: '{{ page.container.label }}' }); {{/ if }} + // Private actions + const submit = async () => { + {{# with (getCreateActionForPage page) as |action| }} + await {{ simpleActionDefinitionName action.actionDefinition }}(); + {{/ with }} + {{# with (getUpdateActionForPage page) as |action| }} + await {{ simpleActionDefinitionName action.actionDefinition }}(); + {{/ with }} + {{# with (getCallOperationActionForPage page) as |action| }} + await {{ simpleActionDefinitionName action.actionDefinition }}(); + {{/ with }} + }; + // Action section {{# each page.actions as |action| }} {{> (getActionTemplate action) }} @@ -208,6 +227,7 @@ export default function {{ pageName page }}() { isFormDeleteable={isFormDeleteable} validation={validation} setValidation={setValidation} + submit={submit} {{/ unless }} {{/ unless }} />