diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c3da3f0e..b3252985 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ jobs: build: name: ⏳ Build, test and deploy artifacts runs-on: judong - timeout-minutes: 5 + timeout-minutes: 7 env: SIGN_KEY_ID: ${{ secrets.GPG_KEYNAME }} SIGN_KEY_PASS: ${{ secrets.GPG_PASSPHRASE }} diff --git a/judo-diff-checker-maven-plugin/LICENSE.txt b/judo-diff-checker-maven-plugin/LICENSE.txt new file mode 100644 index 00000000..d3087e4c --- /dev/null +++ b/judo-diff-checker-maven-plugin/LICENSE.txt @@ -0,0 +1,277 @@ +Eclipse Public License - v 2.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + + a) in the case of the initial Contributor, the initial content + Distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + where such changes and/or additions to the Program originate from + and are Distributed by that particular Contributor. A Contribution + "originates" from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include changes or additions to the Program that + are not Modified Works. + +"Contributor" means any person or entity that Distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which +are necessarily infringed by the use or sale of its Contribution alone +or when combined with the Program. + +"Program" means the Contributions Distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement +or any Secondary License (as applicable), including Contributors. + +"Derivative Works" shall mean any work, whether in Source Code or other +form, that is based on (or derived from) the Program and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. + +"Modified Works" shall mean any work in Source Code or other form that +results from an addition to, deletion from, or modification of the +contents of the Program, including, for purposes of clarity any new file +in Source Code form that contains any contents of the Program. Modified +Works shall not include works that contain only declarations, +interfaces, types, classes, structures, or files of the Program solely +in each case in order to link to, bind by name, or subclass the Program +or Modified Works thereof. + +"Distribute" means the acts of a) distributing or b) making available +in any manner that enables the transfer of a copy. + +"Source Code" means the form of a Program preferred for making +modifications, including but not limited to software source code, +documentation source, and configuration files. + +"Secondary License" means either the GNU General Public License, +Version 2.0, or any later versions of that license, including any +exceptions or additional permissions as identified by the initial +Contributor. + +2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare Derivative Works of, publicly display, + publicly perform, Distribute and sublicense the Contribution of such + Contributor, if any, and such Derivative Works. + + b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, + if any, in Source Code or other form. This patent license shall + apply to the combination of the Contribution and the Program if, at + the time the Contribution is added by the Contributor, such addition + of the Contribution causes such combination to be covered by the + Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. + Each Contributor disclaims any liability to Recipient for claims + brought by any other entity based on infringement of intellectual + property rights or otherwise. As a condition to exercising the + rights and licenses granted hereunder, each Recipient hereby + assumes sole responsibility to secure any other intellectual + property rights needed, if any. For example, if a third party + patent license is required to allow Recipient to Distribute the + Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + + d) Each Contributor represents that to its knowledge it has + sufficient copyright rights in its Contribution, if any, to grant + the copyright license set forth in this Agreement. + + e) Notwithstanding the terms of any Secondary License, no + Contributor makes additional grants to any Recipient (other than + those set forth in this Agreement) as a result of such Recipient's + receipt of the Program under the terms of a Secondary License + (if permitted under the terms of Section 3). + +3. REQUIREMENTS + +3.1 If a Contributor Distributes the Program in any form, then: + + a) the Program must also be made available as Source Code, in + accordance with section 3.2, and the Contributor must accompany + the Program with a statement that the Source Code for the Program + is available under this Agreement, and informs Recipients how to + obtain it in a reasonable manner on or through a medium customarily + used for software exchange; and + + b) the Contributor may Distribute the Program under a license + different than this Agreement, provided that such license: + i) effectively disclaims on behalf of all other Contributors all + warranties and conditions, express and implied, including + warranties or conditions of title and non-infringement, and + implied warranties or conditions of merchantability and fitness + for a particular purpose; + + ii) effectively excludes on behalf of all other Contributors all + liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits; + + iii) does not attempt to limit or alter the recipients' rights + in the Source Code under section 3.2; and + + iv) requires any subsequent distribution of the Program by any + party to be under a license that satisfies the requirements + of this section 3. + +3.2 When the Program is Distributed as Source Code: + + a) it must be made available under this Agreement, or if the + Program (i) is combined with other material in a separate file or + files made available under a Secondary License, and (ii) the initial + Contributor attached to the Source Code the notice described in + Exhibit A of this Agreement, then the Program may be made available + under the terms of such Secondary Licenses, and + + b) a copy of this Agreement must be included with each copy of + the Program. + +3.3 Contributors may not remove or alter any copyright, patent, +trademark, attribution notices, disclaimers of warranty, or limitations +of liability ("notices") contained within the Program from any copy of +the Program which they Distribute, provided that Contributors may add +their own appropriate notices. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities +with respect to end users, business partners and the like. While this +license is intended to facilitate the commercial use of the Program, +the Contributor who includes the Program in a commercial product +offering should do so in a manner which does not create potential +liability for other Contributors. Therefore, if a Contributor includes +the Program in a commercial product offering, such Contributor +("Commercial Contributor") hereby agrees to defend and indemnify every +other Contributor ("Indemnified Contributor") against any losses, +damages and costs (collectively "Losses") arising from claims, lawsuits +and other legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such +Commercial Contributor in connection with its distribution of the Program +in a commercial product offering. The obligations in this section do not +apply to any claims or Losses relating to any actual or alleged +intellectual property infringement. In order to qualify, an Indemnified +Contributor must: a) promptly notify the Commercial Contributor in +writing of such claim, and b) allow the Commercial Contributor to control, +and cooperate with the Commercial Contributor in, the defense and any +related settlement negotiations. The Indemnified Contributor may +participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those performance +claims and warranties, and if a court requires any other Contributor to +pay any damages as a result, the Commercial Contributor must pay +those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" +BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF +TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR +PURPOSE. Each Recipient is solely responsible for determining the +appropriateness of using and distributing the Program and assumes all +risks associated with its exercise of rights under this Agreement, +including but not limited to the risks and costs of program errors, +compliance with applicable laws, damage to or loss of data, programs +or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS +SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST +PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE +EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further +action by the parties hereto, such provision shall be reformed to the +minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity +(including a cross-claim or counterclaim in a lawsuit) alleging that the +Program itself (excluding combinations of the Program with other software +or hardware) infringes such Recipient's patent(s), then such Recipient's +rights granted under Section 2(b) shall terminate as of the date such +litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it +fails to comply with any of the material terms or conditions of this +Agreement and does not cure such failure in a reasonable period of +time after becoming aware of such noncompliance. If all Recipient's +rights under this Agreement terminate, Recipient agrees to cease use +and distribution of the Program as soon as reasonably practicable. +However, Recipient's obligations under this Agreement and any licenses +granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, +but in order to avoid inconsistency the Agreement is copyrighted and +may only be modified in the following manner. The Agreement Steward +reserves the right to publish new versions (including revisions) of +this Agreement from time to time. No one other than the Agreement +Steward has the right to modify this Agreement. The Eclipse Foundation +is the initial Agreement Steward. The Eclipse Foundation may assign the +responsibility to serve as the Agreement Steward to a suitable separate +entity. Each new version of the Agreement will be given a distinguishing +version number. The Program (including Contributions) may always be +Distributed subject to the version of the Agreement under which it was +received. In addition, after a new version of the Agreement is published, +Contributor may elect to Distribute the Program (including its +Contributions) under the new version. + +Except as expressly stated in Sections 2(a) and 2(b) above, Recipient +receives no rights or licenses to the intellectual property of any +Contributor under this Agreement, whether expressly, by implication, +estoppel or otherwise. All rights in the Program not expressly granted +under this Agreement are reserved. Nothing in this Agreement is intended +to be enforceable by any entity that is not a Contributor or Recipient. +No third-party beneficiary rights are created under this Agreement. + +Exhibit A - Form of Secondary Licenses Notice + +"This Source Code may also be made available under the following +Secondary Licenses when the conditions for such availability set forth +in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), +version(s), and exceptions or additional permissions here}." + + Simply including a copy of this Agreement, including this Exhibit A + is not sufficient to license the Source Code under Secondary Licenses. + + If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE + file in a relevant directory) where a recipient would be likely to + look for such a notice. + + You may add additional accurate notices of copyright ownership. diff --git a/judo-diff-checker-maven-plugin/README.adoc b/judo-diff-checker-maven-plugin/README.adoc new file mode 100644 index 00000000..a640d67d --- /dev/null +++ b/judo-diff-checker-maven-plugin/README.adoc @@ -0,0 +1,52 @@ += judo-diff-checker-maven-plugin + +Check diff for any number of files, and highlight them. + +== Usage + +[source,xml] +---- + + hu.blackbelt.judo.generator + judo-diff-checker-maven-plugin + ${revision} + + + + checkDiffs + + generate-sources + + + + ${project.basedir}/target/frontend-react/ + ${project.basedir}/src/test/resources/snapshots/frontend-react/ + + src/pages/God/God/Galaxies/AccessViewPage/index.tsx + src/pages/God/God/Galaxies/AccessTablePage/index.tsx + + + + +---- + +== Configuration + +=== createSnapshotIfNotExists + +(Optional, default: `true`) + +The plugin will automatically create snapshot files if they do not exist, unless the `createSnapshotIfNotExists` +configuration is explicitly set to `false`; + +=== forceSnapshotOverwrite + +(Optional, default: `false`) + +If you would like the plugin to always update the contents of snapshot files before checking, set this to `true`. + +Setting it to true can be useful if you are e.g. doing refactors and do not intend to have any changes in your snapshot. + +=== snapshotPostfix + +(Optional, default: `".snapshot"`) diff --git a/judo-diff-checker-maven-plugin/pom.xml b/judo-diff-checker-maven-plugin/pom.xml new file mode 100644 index 00000000..c9f28aa2 --- /dev/null +++ b/judo-diff-checker-maven-plugin/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + hu.blackbelt.judo.generator + judo-ui-react-template + ${revision} + + + judo-diff-checker-maven-plugin + maven-plugin + + JUDO Diff Checker Maven Plugin + Tooling to check for diffs in files + + + + org.apache.maven.plugin-tools + maven-plugin-annotations + 3.10.2 + provided + + + org.apache.maven + maven-plugin-api + 3.9.6 + + + io.github.java-diff-utils + java-diff-utils + 4.12 + + + + + + + org.apache.maven.plugins + maven-plugin-plugin + 3.8.2 + + snapshot-checker + + + + + descriptor + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + ${maven.compiler.source} + ${maven.compiler.source} + + + + + diff --git a/judo-diff-checker-maven-plugin/src/main/java/hu/blackbelt/judo/diffchecker/DiffCheckerMojo.java b/judo-diff-checker-maven-plugin/src/main/java/hu/blackbelt/judo/diffchecker/DiffCheckerMojo.java new file mode 100644 index 00000000..3eabb70c --- /dev/null +++ b/judo-diff-checker-maven-plugin/src/main/java/hu/blackbelt/judo/diffchecker/DiffCheckerMojo.java @@ -0,0 +1,107 @@ +package hu.blackbelt.judo.diffchecker; + +import com.github.difflib.DiffUtils; +import com.github.difflib.UnifiedDiffUtils; +import com.github.difflib.patch.Patch; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.List; + +@Mojo(name = "checkDiffs", defaultPhase = LifecyclePhase.VERIFY) +public class DiffCheckerMojo extends AbstractMojo { + + @Parameter(property = "sources") + private List sources; + + @Parameter(property = "snapshotPostfix", defaultValue = ".snapshot") + private String snapshotPostfix = ".snapshot"; + + @Parameter(property = "forceSnapshotOverwrite", defaultValue = "false") + private Boolean forceSnapshotOverwrite = false; + + @Parameter(property = "sourceDirectory", required = true) + private String sourceDirectory; + + @Parameter(property = "snapshotDirectory", required = true) + private String snapshotDirectory; + + @Parameter(property = "createSnapshotIfNotExists", defaultValue = "true") + private Boolean createSnapshotIfNotExists = true; + + private static final int CONTEXT_SIZE = 3; + + @Override + public void execute() throws MojoExecutionException { + Path sourcePath = Paths.get(sourceDirectory); + Path snapshotPath = Paths.get(snapshotDirectory); + + if (sources == null || sources.isEmpty()) { + throw new MojoExecutionException("No sources configured for snapshot checking."); + } + + for (String source : sources) { + Path sourceFilePath = sourcePath.resolve(source); + Path snapshotFilePath = snapshotPath.resolve(source + snapshotPostfix); + File sourceFile = sourceFilePath.toFile(); + File snapshotFile = snapshotFilePath.toFile(); + + if (!sourceFile.exists()) { + throw new MojoExecutionException("Source file does not exist: " + sourceFile.getPath()); + } + + try { + if (!snapshotFile.exists()) { + if (createSnapshotIfNotExists) { + createSnapshotFileFromSource(sourceFile, snapshotFile); + getLog().info("Created snapshot file: " + snapshotFile.getPath()); + } else { + throw new MojoExecutionException("Snapshot file does not exist: " + snapshotFile.getPath()); + } + } else { + if (forceSnapshotOverwrite) { + createSnapshotFileFromSource(sourceFile, snapshotFile); + getLog().info("Overwritten snapshot file: " + snapshotFile.getPath()); + } + compareFiles(sourceFile, snapshotFile); + } + } catch (IOException e) { + throw new MojoExecutionException("Error processing files: " + sourceFile.getPath() + " and " + snapshotFile.getPath(), e); + } + } + } + + private void createSnapshotFileFromSource(File sourceFile, File snapshotFile) throws IOException { + Files.createDirectories(snapshotFile.getParentFile().toPath()); + Files.copy(sourceFile.toPath(), snapshotFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + + private void compareFiles(File sourceFile, File snapshotFile) throws IOException, MojoExecutionException { + List sourceLines = Files.readAllLines(sourceFile.toPath()); + List snapshotLines = Files.readAllLines(snapshotFile.toPath()); + + Patch diff = DiffUtils.diff(snapshotLines, sourceLines); + + if (!diff.getDeltas().isEmpty()) { + displayGitLikeDiff(snapshotLines, sourceLines, "[SNAPSHOT] " + snapshotFile.getPath().substring(snapshotDirectory.length()), "[SOURCE] " + sourceFile.getPath().substring(sourceDirectory.length())); + throw new MojoExecutionException("Snapshot changes detected in " + sourceFile.getPath()); + } + } + + private void displayGitLikeDiff(List original, List revised, String snapshotPath, String sourcePath) { + Patch patch = DiffUtils.diff(original, revised); + List unifiedDiff = UnifiedDiffUtils.generateUnifiedDiff(snapshotPath, sourcePath, original, patch, CONTEXT_SIZE); + for (String line : unifiedDiff) { + System.out.println(line); + } + } +} diff --git a/judo-ui-react-itest/ActionGroupTest/action_group_test__god/pom.xml b/judo-ui-react-itest/ActionGroupTest/action_group_test__god/pom.xml index 9f8bb3a5..12e3af75 100644 --- a/judo-ui-react-itest/ActionGroupTest/action_group_test__god/pom.xml +++ b/judo-ui-react-itest/ActionGroupTest/action_group_test__god/pom.xml @@ -93,7 +93,7 @@ ${ui-model} ${generation-target} - true + false ${model-name} ${appScope} ${appVersion} @@ -179,7 +179,7 @@ pnpm - generate-sources + test run build @@ -201,6 +201,36 @@ ${generation-target} + + + hu.blackbelt.judo.generator + judo-diff-checker-maven-plugin + ${revision} + + + + checkDiffs + + generate-sources + + + + ${project.basedir}/target/frontend-react/ + ${project.basedir}/src/test/resources/snapshots/frontend-react/ + + src/pages/God/God/Galaxies/AccessViewPage/index.tsx + src/pages/God/God/Galaxies/AccessTablePage/index.tsx + src/containers/View/Galaxy/Form/ViewGalaxyForm.tsx + src/containers/View/Galaxy/Form/ViewGalaxyFormDialogContainer.tsx + src/containers/View/Galaxy/View/ViewGalaxyView.tsx + src/containers/View/Galaxy/View/ViewGalaxyViewDialogContainer.tsx + src/containers/View/Galaxy/View/ViewGalaxyViewPageContainer.tsx + src/containers/Planet/View/PlanetView.tsx + src/containers/Planet/View/PlanetViewPageContainer.tsx + src/pages/God/God/Earth/AccessViewPage/index.tsx + + + diff --git a/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/containers/Planet/View/PlanetView.tsx.snapshot b/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/containers/Planet/View/PlanetView.tsx.snapshot new file mode 100644 index 00000000..44d03a1b --- /dev/null +++ b/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/containers/Planet/View/PlanetView.tsx.snapshot @@ -0,0 +1,130 @@ +////////////////////////////////////////////////////////////////////////////// +// G E N E R A T E D S O U R C E +// -------------------------------- +// Factory expression: #application.pageContainers +// Path expression: 'src/containers/'+#containerPath(#self)+'/'+#containerComponentName(#self)+'.tsx' +// Template name: actor/src/containers/container.tsx +// Template file: actor/src/containers/container.tsx.hbs + +import LoadingButton from '@mui/lab/LoadingButton'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import ButtonGroup from '@mui/material/ButtonGroup'; +import Container from '@mui/material/Container'; +import Grid from '@mui/material/Grid'; +import IconButton from '@mui/material/IconButton'; +import InputAdornment from '@mui/material/InputAdornment'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import { OBJECTCLASS } from '@pandino/pandino-api'; +import { useTrackService } from '@pandino/react-hooks'; +import { clsx } from 'clsx'; +import type { Dispatch, FC, SetStateAction } from 'react'; +import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { DropdownButton, MdiIcon, useJudoNavigation } from '~/components'; +import { useConfirmDialog } from '~/components/dialog'; +import { useL10N } from '~/l10n/l10n-context'; +import { isErrorOperationFault, useErrorHandler } from '~/utilities'; + +import {} from '@mui/x-date-pickers'; +import type {} from '@mui/x-date-pickers'; +import {} from '~/components/widgets'; +import { useConfirmationBeforeChange } from '~/hooks'; +import type { Planet, PlanetStored } from '~/services/data-api/model/Planet'; +import { PLANET_VIEW_CONTAINER_ACTIONS_HOOK_INTERFACE_KEY } from './customization'; +import type { PlanetViewContainerHook } from './customization'; +import type { PlanetViewActionDefinitions, PlanetViewProps } from './types'; + +// XMIID: God/(esm/_iyJ2kORqEeuSU8xLq1yYbw)/TransferObjectViewPageContainer +// Name: Planet::View +export default function PlanetView(props: PlanetViewProps) { + // Container props + const { + refreshCounter, + isLoading, + isDraft, + dataPath, + actions: pageActions, + data, + isFormUpdateable, + isFormDeleteable, + storeDiff, + editMode, + validation, + setValidation, + submit, + } = props; + + // Container hooks + const { t } = useTranslation(); + const { navigate, back } = useJudoNavigation(); + const { locale: l10nLocale } = useL10N(); + const { openConfirmDialog } = useConfirmDialog(); + + useConfirmationBeforeChange( + editMode, + t('judo.form.navigation.confirmation', { + defaultValue: 'You have potential unsaved changes in your form, are you sure you would like to navigate away?', + }), + ); + // Pandino Container Action overrides + const { service: customContainerHook } = useTrackService( + `(${OBJECTCLASS}=${PLANET_VIEW_CONTAINER_ACTIONS_HOOK_INTERFACE_KEY})`, + ); + const containerActions: PlanetViewActionDefinitions = customContainerHook?.(data, editMode, storeDiff) || {}; + const actions = useMemo(() => ({ ...pageActions, ...containerActions }), [pageActions, containerActions]); + + return ( + + + + + + + { + const realValue = event.target.value?.length === 0 ? null : event.target.value; + storeDiff('name', realValue); + }} + InputLabelProps={{ shrink: true }} + InputProps={{ + readOnly: false || !isFormUpdateable(), + startAdornment: ( + + + + ), + }} + inputProps={{ + maxLength: 255, + }} + /> + + + + + + + ); +} diff --git a/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/containers/Planet/View/PlanetViewPageContainer.tsx.snapshot b/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/containers/Planet/View/PlanetViewPageContainer.tsx.snapshot new file mode 100644 index 00000000..099bd7f5 --- /dev/null +++ b/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/containers/Planet/View/PlanetViewPageContainer.tsx.snapshot @@ -0,0 +1,163 @@ +////////////////////////////////////////////////////////////////////////////// +// G E N E R A T E D S O U R C E +// -------------------------------- +// Factory expression: #getPageContainersToGenerate(#application) +// Path expression: 'src/containers/'+#containerPath(#self)+'/'+#containerComponentName(#self)+'PageContainer.tsx' +// Template name: actor/src/containers/page.tsx +// Template file: actor/src/containers/page.tsx.hbs + +import LoadingButton from '@mui/lab/LoadingButton'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Grid from '@mui/material/Grid'; +import { Suspense, lazy } from 'react'; +import type { Dispatch, SetStateAction } from 'react'; +import { useTranslation } from 'react-i18next'; +import { MdiIcon, PageHeader, useJudoNavigation } from '~/components'; +import { useConfirmDialog } from '~/components/dialog'; +import type { Planet, PlanetStored } from '~/services/data-api/model/Planet'; +import type { PlanetQueryCustomizer } from '~/services/data-api/rest/PlanetQueryCustomizer'; +import { mainContainerPadding } from '~/theme'; +import { processQueryCustomizer } from '~/utilities'; +import { getValue } from '~/utilities/helper'; + +import type { PlanetViewActionDefinitions, PlanetViewPageActions, PlanetViewPageProps } from './types'; + +const PlanetView = lazy(() => import('~/containers/Planet/View/PlanetView')); + +// Name: Planet::View +export default function PlanetViewPage(props: PlanetViewPageProps) { + const { openConfirmDialog } = useConfirmDialog(); + + const { t } = useTranslation(); + const { navigate, back } = useJudoNavigation(); + const { + actions, + dataPath, + isLoading, + editMode, + refreshCounter, + data, + isFormUpdateable, + isFormDeleteable, + storeDiff, + validation, + setValidation, + submit, + } = props; + const isDraft = false; // currently no page can be opened as draft, but we need this variable anyway + const queryCustomizer: PlanetQueryCustomizer = { + _mask: actions.getMask?.(), + }; + + return ( + <> + + {!editMode && actions.backAction && ( + + } + disabled={isLoading || editMode} + onClick={async () => { + await actions.backAction!(); + }} + > + {t('judo.action.back', { defaultValue: 'Back' })} + + + )} + {!isDraft && actions.refreshAction && ( + + } + disabled={isLoading || editMode} + onClick={async () => { + await actions.refreshAction!(processQueryCustomizer(queryCustomizer)); + }} + > + {t('judo.action.refresh', { defaultValue: 'Refresh' })} + + + )} + {!editMode && (isFormDeleteable() || isDraft) && actions.deleteAction && ( + + } + disabled={isLoading || editMode} + onClick={async () => { + actions.deleteAction!(); + }} + > + {t('judo.action.delete', { defaultValue: 'Delete' })} + + + )} + {editMode && actions.cancelAction && ( + + } + disabled={isLoading} + onClick={async () => { + await actions.cancelAction!(); + }} + > + {t('judo.action.cancel', { defaultValue: 'Cancel' })} + + + )} + {editMode && actions.updateAction && ( + + } + disabled={isLoading} + onClick={async () => { + await actions.updateAction!(); + }} + > + {t('judo.action.update', { defaultValue: 'Save' })} + + + )} +
{/* Placeholder */}
+
+ + + + + + + ); +} diff --git a/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/containers/View/Galaxy/Form/ViewGalaxyForm.tsx.snapshot b/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/containers/View/Galaxy/Form/ViewGalaxyForm.tsx.snapshot new file mode 100644 index 00000000..598572ac --- /dev/null +++ b/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/containers/View/Galaxy/Form/ViewGalaxyForm.tsx.snapshot @@ -0,0 +1,392 @@ +////////////////////////////////////////////////////////////////////////////// +// G E N E R A T E D S O U R C E +// -------------------------------- +// Factory expression: #application.pageContainers +// Path expression: 'src/containers/'+#containerPath(#self)+'/'+#containerComponentName(#self)+'.tsx' +// Template name: actor/src/containers/container.tsx +// Template file: actor/src/containers/container.tsx.hbs + +import LoadingButton from '@mui/lab/LoadingButton'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import ButtonGroup from '@mui/material/ButtonGroup'; +import Checkbox from '@mui/material/Checkbox'; +import Container from '@mui/material/Container'; +import FormControl from '@mui/material/FormControl'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormGroup from '@mui/material/FormGroup'; +import FormHelperText from '@mui/material/FormHelperText'; +import Grid from '@mui/material/Grid'; +import IconButton from '@mui/material/IconButton'; +import InputAdornment from '@mui/material/InputAdornment'; +import MenuItem from '@mui/material/MenuItem'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import { OBJECTCLASS } from '@pandino/pandino-api'; +import { useTrackService } from '@pandino/react-hooks'; +import { clsx } from 'clsx'; +import type { Dispatch, FC, SetStateAction } from 'react'; +import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { DropdownButton, MdiIcon, useJudoNavigation } from '~/components'; +import { useConfirmDialog } from '~/components/dialog'; +import { useL10N } from '~/l10n/l10n-context'; +import { isErrorOperationFault, useErrorHandler } from '~/utilities'; + +import { DateTimePicker } from '@mui/x-date-pickers'; +import type { DateTimeValidationError } from '@mui/x-date-pickers'; +import { NumericInput } from '~/components/widgets'; +import { autoFocusRefDelay } from '~/config'; +import { useConfirmationBeforeChange } from '~/hooks'; +import type { ViewAstronomer, ViewAstronomerStored } from '~/services/data-api/model/ViewAstronomer'; +import type { ViewGalaxy, ViewGalaxyStored } from '~/services/data-api/model/ViewGalaxy'; +import { ViewGalaxyFormAstronomerComponent } from './components/ViewGalaxyFormAstronomerComponent'; +import { VIEW_GALAXY_FORM_CONTAINER_ACTIONS_HOOK_INTERFACE_KEY } from './customization'; +import type { ViewGalaxyFormContainerHook } from './customization'; +import type { ViewGalaxyFormActionDefinitions, ViewGalaxyFormProps } from './types'; + +// XMIID: God/(esm/_YTwP0E7rEeycO-gUAWxcVg)/TransferObjectFormPageContainer +// Name: View::Galaxy::Form +export default function ViewGalaxyForm(props: ViewGalaxyFormProps) { + // Container props + const { + refreshCounter, + isLoading, + isDraft, + dataPath, + actions: pageActions, + data, + isFormUpdateable, + isFormDeleteable, + storeDiff, + editMode, + validation, + setValidation, + submit, + } = props; + + // Container hooks + const { t } = useTranslation(); + const { navigate, back } = useJudoNavigation(); + const { locale: l10nLocale } = useL10N(); + const { openConfirmDialog } = useConfirmDialog(); + + useConfirmationBeforeChange( + editMode, + t('judo.form.navigation.confirmation', { + defaultValue: 'You have potential unsaved changes in your form, are you sure you would like to navigate away?', + }), + ); + // Pandino Container Action overrides + const { service: customContainerHook } = useTrackService( + `(${OBJECTCLASS}=${VIEW_GALAXY_FORM_CONTAINER_ACTIONS_HOOK_INTERFACE_KEY})`, + ); + const containerActions: ViewGalaxyFormActionDefinitions = customContainerHook?.(data, editMode, storeDiff) || {}; + const actions = useMemo(() => ({ ...pageActions, ...containerActions }), [pageActions, containerActions]); + const autoFocusInputRef = useRef(null); + + useEffect(() => { + const timeout = setTimeout(() => { + if (typeof autoFocusInputRef?.current?.focus === 'function') { + autoFocusInputRef.current.focus(); + } + }, autoFocusRefDelay); + + return () => clearTimeout(timeout); + }, []); + + return ( + + + + + + + { + const realValue = event.target.value?.length === 0 ? null : event.target.value; + storeDiff('name', realValue); + }} + InputLabelProps={{ shrink: true }} + InputProps={{ + readOnly: false || !isFormUpdateable(), + startAdornment: ( + + + + ), + }} + inputProps={{ + maxLength: 255, + }} + /> + + + + + + (validation.has('real') ? theme.palette.error.main : 'primary'), + }} + disabled={ + actions?.isRealDisabled + ? actions.isRealDisabled(data, editMode, isLoading) + : false || !isFormUpdateable() || isLoading + } + control={ + (validation.has('real') ? theme.palette.error.main : 'primary') }} + onChange={(event) => { + storeDiff('real', event.target.checked); + }} + /> + } + label={t('View.Galaxy.Form.real', { defaultValue: 'Real' }) as string} + /> + + {validation.has('real') && {validation.get('real')}} + + + + + { + const realValue = event.target.value?.length === 0 ? null : event.target.value; + storeDiff('constellation', realValue); + }} + InputLabelProps={{ shrink: true }} + InputProps={{ + readOnly: false || !isFormUpdateable(), + startAdornment: ( + + + + ), + }} + inputProps={{ + maxLength: 255, + }} + /> + + + + + + (validation.has('nakedEye') ? theme.palette.error.main : 'primary'), + }} + disabled={ + actions?.isNakedEyeDisabled + ? actions.isNakedEyeDisabled(data, editMode, isLoading) + : false || !isFormUpdateable() || isLoading + } + control={ + (validation.has('nakedEye') ? theme.palette.error.main : 'primary') }} + onChange={(event) => { + storeDiff('nakedEye', event.target.checked); + }} + /> + } + label={t('View.Galaxy.Form.nakedEye', { defaultValue: 'Naked Eye' }) as string} + /> + + {validation.has('nakedEye') && {validation.get('nakedEye')}} + + + + + { + const newValue = values.floatValue === undefined ? null : values.floatValue; + if (data.magnitude !== newValue) { + storeDiff('magnitude', newValue); + } + }} + InputLabelProps={{ shrink: true }} + InputProps={{ + readOnly: false || !isFormUpdateable(), + startAdornment: ( + + + + ), + }} + /> + + + + { + const realValue = event.target.value?.length === 0 ? null : event.target.value; + storeDiff('originOfName', realValue); + }} + InputLabelProps={{ shrink: true }} + InputProps={{ + readOnly: false || !isFormUpdateable(), + startAdornment: ( + + + + ), + }} + inputProps={{ + maxLength: 255, + }} + /> + + + + + + + ), + }, + }, + actionBar: { + actions: ['today', 'accept'], + }, + }} + onError={(newError: DateTimeValidationError, value: any) => { + // https://mui.com/x/react-date-pickers/validation/#show-the-error + setValidation((prevValidation) => { + const copy = new Map(prevValidation); + copy.set( + 'discovered', + newError === 'invalidDate' + ? (t('judo.error.validation-failed.PATTERN_VALIDATION_FAILED', { + defaultValue: 'Value does not match the pattern requirements.', + }) as string) + : '', + ); + return copy; + }); + }} + views={['year', 'month', 'day', 'hours', 'minutes', 'seconds']} + label={t('View.Galaxy.Form.discovered', { defaultValue: 'Discovered' }) as string} + value={data.discovered ?? null} + readOnly={false || !isFormUpdateable()} + disabled={ + actions?.isDiscoveredDisabled ? actions.isDiscoveredDisabled(data, editMode, isLoading) : isLoading + } + onChange={(newValue: Date | null) => { + storeDiff('discovered', newValue); + }} + /> + + + + + + + + + + + ); +} diff --git a/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/containers/View/Galaxy/Form/ViewGalaxyFormDialogContainer.tsx.snapshot b/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/containers/View/Galaxy/Form/ViewGalaxyFormDialogContainer.tsx.snapshot new file mode 100644 index 00000000..faacbd1b --- /dev/null +++ b/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/containers/View/Galaxy/Form/ViewGalaxyFormDialogContainer.tsx.snapshot @@ -0,0 +1,200 @@ +////////////////////////////////////////////////////////////////////////////// +// G E N E R A T E D S O U R C E +// -------------------------------- +// Factory expression: #application.pageContainers +// Path expression: 'src/containers/'+#containerPath(#self)+'/'+#containerComponentName(#self)+'DialogContainer.tsx' +// Template name: actor/src/containers/dialog.tsx +// Template file: actor/src/containers/dialog.tsx.hbs + +import LoadingButton from '@mui/lab/LoadingButton'; +import Button from '@mui/material/Button'; +import ButtonGroup from '@mui/material/ButtonGroup'; +import ClickAwayListener from '@mui/material/ClickAwayListener'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import Grid from '@mui/material/Grid'; +import Grow from '@mui/material/Grow'; +import IconButton from '@mui/material/IconButton'; +import MenuItem from '@mui/material/MenuItem'; +import MenuList from '@mui/material/MenuList'; +import Paper from '@mui/material/Paper'; +import Popper from '@mui/material/Popper'; +import { Suspense, lazy, useCallback, useRef, useState } from 'react'; +import type { Dispatch, SetStateAction } from 'react'; +import { useTranslation } from 'react-i18next'; +import { MdiIcon, useJudoNavigation } from '~/components'; +import { useConfirmDialog } from '~/components/dialog'; +import type { ViewGalaxy, ViewGalaxyStored } from '~/services/data-api/model/ViewGalaxy'; +import type { ViewGalaxyQueryCustomizer } from '~/services/data-api/rest/ViewGalaxyQueryCustomizer'; +import { processQueryCustomizer } from '~/utilities'; +import { getValue } from '~/utilities/helper'; + +import type { ViewGalaxyFormActionDefinitions, ViewGalaxyFormDialogActions, ViewGalaxyFormDialogProps } from './types'; + +const ViewGalaxyForm = lazy(() => import('~/containers/View/Galaxy/Form/ViewGalaxyForm')); + +// Name: View::Galaxy::Form +export default function ViewGalaxyFormDialog(props: ViewGalaxyFormDialogProps) { + const { openConfirmDialog } = useConfirmDialog(); + + const { t } = useTranslation(); + const { navigate, back } = useJudoNavigation(); + const [isCreateDropdownOpen, setIsCreateDropdownOpen] = useState(false); + const createDropdownRef = useRef(null); + const { + ownerData, + onClose, + actions, + dataPath, + isLoading, + editMode, + refreshCounter, + data, + isFormUpdateable, + isFormDeleteable, + storeDiff, + validation, + setValidation, + submit, + isDraft, + } = props; + + const handleCreateDropdownToggle = useCallback(() => { + setIsCreateDropdownOpen((prevOpen) => !prevOpen); + }, [isCreateDropdownOpen]); + const handleCreateDropdownClose = useCallback(() => { + setIsCreateDropdownOpen(false); + }, [isCreateDropdownOpen]); + + return ( + <> + + {actions.getPageTitle ? actions.getPageTitle(data) : ''} + theme.palette.grey[500], + }} + > + + + + + + + + + + {editMode && actions.backAction && ( + + } + onClick={async () => { + await actions.backAction!(); + }} + > + {t('judo.action.back', { defaultValue: 'Back' })} + + + )} + {editMode && actions.createAction && ( + + + : } + onClick={async () => { + await actions.createAction!(); + }} + > + + {isDraft + ? t('judo.dialogs.draft.submit', { defaultValue: 'Ok' }) + : t('judo.action.create', { defaultValue: 'Create' })} + + + {!isDraft && ( + + )} + + + {({ TransitionProps, placement }) => ( + + + + + { + if (actions.createAction) { + await actions.createAction!(true); + } + }} + > + {t('judo.pages.create-and-navigate', { defaultValue: 'Create and open' })} + + + + + + )} + + + )} + + + ); +} diff --git a/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/containers/View/Galaxy/View/ViewGalaxyView.tsx.snapshot b/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/containers/View/Galaxy/View/ViewGalaxyView.tsx.snapshot new file mode 100644 index 00000000..df5aa4ad --- /dev/null +++ b/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/containers/View/Galaxy/View/ViewGalaxyView.tsx.snapshot @@ -0,0 +1,826 @@ +////////////////////////////////////////////////////////////////////////////// +// G E N E R A T E D S O U R C E +// -------------------------------- +// Factory expression: #application.pageContainers +// Path expression: 'src/containers/'+#containerPath(#self)+'/'+#containerComponentName(#self)+'.tsx' +// Template name: actor/src/containers/container.tsx +// Template file: actor/src/containers/container.tsx.hbs + +import LoadingButton from '@mui/lab/LoadingButton'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import ButtonGroup from '@mui/material/ButtonGroup'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import Checkbox from '@mui/material/Checkbox'; +import Container from '@mui/material/Container'; +import FormControl from '@mui/material/FormControl'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormGroup from '@mui/material/FormGroup'; +import FormHelperText from '@mui/material/FormHelperText'; +import Grid from '@mui/material/Grid'; +import IconButton from '@mui/material/IconButton'; +import InputAdornment from '@mui/material/InputAdornment'; +import MenuItem from '@mui/material/MenuItem'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import { OBJECTCLASS } from '@pandino/pandino-api'; +import { ComponentProxy } from '@pandino/react-hooks'; +import { useTrackService } from '@pandino/react-hooks'; +import { clsx } from 'clsx'; +import type { Dispatch, FC, SetStateAction } from 'react'; +import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { DropdownButton, MdiIcon, useJudoNavigation } from '~/components'; +import { useConfirmDialog } from '~/components/dialog'; +import { CUSTOM_VISUAL_ELEMENT_INTERFACE_KEY } from '~/custom'; +import type { CustomFormVisualElementProps } from '~/custom'; +import { useL10N } from '~/l10n/l10n-context'; +import { isErrorOperationFault, useErrorHandler } from '~/utilities'; + +import { DateTimePicker } from '@mui/x-date-pickers'; +import type { DateTimeValidationError } from '@mui/x-date-pickers'; +import { AssociationButton, NumericInput } from '~/components/widgets'; +import { useConfirmationBeforeChange } from '~/hooks'; +import type { ViewAstronomer, ViewAstronomerStored } from '~/services/data-api/model/ViewAstronomer'; +import type { ViewGalaxy, ViewGalaxyStored } from '~/services/data-api/model/ViewGalaxy'; +import type { ViewMatter, ViewMatterStored } from '~/services/data-api/model/ViewMatter'; +import type { ViewStar, ViewStarStored } from '~/services/data-api/model/ViewStar'; +import { ViewGalaxyViewAstronomerComponent } from './components/ViewGalaxyViewAstronomerComponent'; +import { ViewGalaxyViewStarsComponent } from './components/ViewGalaxyViewStarsComponent'; +import { + VIEW_GALAXY_VIEW_CONTAINER_ACTIONS_HOOK_INTERFACE_KEY, + VIEW_GALAXY_VIEW_DISCOVERER_COMPONENT, +} from './customization'; +import type { ViewGalaxyViewContainerHook } from './customization'; +import type { ViewGalaxyViewActionDefinitions, ViewGalaxyViewProps } from './types'; + +// XMIID: God/(esm/_YTr-YE7rEeycO-gUAWxcVg)/TransferObjectViewPageContainer +// Name: View::Galaxy::View +export default function ViewGalaxyView(props: ViewGalaxyViewProps) { + // Container props + const { + refreshCounter, + isLoading, + isDraft, + dataPath, + actions: pageActions, + data, + isFormUpdateable, + isFormDeleteable, + storeDiff, + editMode, + validation, + setValidation, + submit, + } = props; + + // Container hooks + const { t } = useTranslation(); + const { navigate, back } = useJudoNavigation(); + const { locale: l10nLocale } = useL10N(); + const { openConfirmDialog } = useConfirmDialog(); + + useConfirmationBeforeChange( + editMode, + t('judo.form.navigation.confirmation', { + defaultValue: 'You have potential unsaved changes in your form, are you sure you would like to navigate away?', + }), + ); + // Pandino Container Action overrides + const { service: customContainerHook } = useTrackService( + `(${OBJECTCLASS}=${VIEW_GALAXY_VIEW_CONTAINER_ACTIONS_HOOK_INTERFACE_KEY})`, + ); + const containerActions: ViewGalaxyViewActionDefinitions = customContainerHook?.(data, editMode, storeDiff) || {}; + const actions = useMemo(() => ({ ...pageActions, ...containerActions }), [pageActions, containerActions]); + + return ( + + + + + + + + + + + + + + + + + + {t('View.Galaxy.View.group', { defaultValue: 'Stars' })} + + + + + + + + + + + + + + + + + + + + + + + + + + + + {t('View.Galaxy.View.group_2', { defaultValue: 'About' })} + + + + + + + + + + + { + const realValue = + event.target.value?.length === 0 ? null : event.target.value; + storeDiff('name', realValue); + }} + InputLabelProps={{ shrink: true }} + InputProps={{ + readOnly: false || !isFormUpdateable(), + startAdornment: ( + + + + ), + }} + inputProps={{ + maxLength: 255, + }} + /> + + + + + + + validation.has('real') ? theme.palette.error.main : 'primary', + }} + disabled={ + actions?.isRealDisabled + ? actions.isRealDisabled(data, editMode, isLoading) + : false || !isFormUpdateable() || isLoading + } + control={ + + validation.has('real') ? theme.palette.error.main : 'primary', + }} + onChange={(event) => { + storeDiff('real', event.target.checked); + }} + /> + } + label={t('View.Galaxy.View.real', { defaultValue: 'Real' }) as string} + /> + + {validation.has('real') && ( + {validation.get('real')} + )} + + + + + + + + + { + const realValue = event.target.value?.length === 0 ? null : event.target.value; + storeDiff('constellation', realValue); + }} + InputLabelProps={{ shrink: true }} + InputProps={{ + readOnly: false || !isFormUpdateable(), + startAdornment: ( + + + + ), + }} + inputProps={{ + maxLength: 255, + }} + /> + + + + { + const newValue = values.floatValue === undefined ? null : values.floatValue; + if (data.magnitude !== newValue) { + storeDiff('magnitude', newValue); + } + }} + InputLabelProps={{ shrink: true }} + InputProps={{ + readOnly: false || !isFormUpdateable(), + startAdornment: ( + + + + ), + }} + /> + + + + + + + validation.has('nakedEye') ? theme.palette.error.main : 'primary', + }} + disabled={ + actions?.isNakedEyeDisabled + ? actions.isNakedEyeDisabled(data, editMode, isLoading) + : false || !isFormUpdateable() || isLoading + } + control={ + + validation.has('nakedEye') ? theme.palette.error.main : 'primary', + }} + onChange={(event) => { + storeDiff('nakedEye', event.target.checked); + }} + /> + } + label={t('View.Galaxy.View.nakedEye', { defaultValue: 'Naked Eye' }) as string} + /> + + {validation.has('nakedEye') && ( + {validation.get('nakedEye')} + )} + + + + + { + const realValue = event.target.value?.length === 0 ? null : event.target.value; + storeDiff('originOfName', realValue); + }} + InputLabelProps={{ shrink: true }} + InputProps={{ + readOnly: false || !isFormUpdateable(), + startAdornment: ( + + + + ), + }} + inputProps={{ + maxLength: 255, + }} + /> + + + + + + + + + + + + + + + + {t('View.Galaxy.View.Discoverer', { defaultValue: 'Discoverer' })} + + + + + + + + + + ), + }, + }, + actionBar: { + actions: ['today', 'accept'], + }, + }} + onError={(newError: DateTimeValidationError, value: any) => { + // https://mui.com/x/react-date-pickers/validation/#show-the-error + setValidation((prevValidation) => { + const copy = new Map(prevValidation); + copy.set( + 'discovered', + newError === 'invalidDate' + ? (t('judo.error.validation-failed.PATTERN_VALIDATION_FAILED', { + defaultValue: 'Value does not match the pattern requirements.', + }) as string) + : '', + ); + return copy; + }); + }} + views={['year', 'month', 'day', 'hours', 'minutes', 'seconds']} + label={t('View.Galaxy.View.discovered', { defaultValue: 'Discovered' }) as string} + value={data.discovered ?? null} + readOnly={false || !isFormUpdateable()} + disabled={ + actions?.isDiscoveredDisabled + ? actions.isDiscoveredDisabled(data, editMode, isLoading) + : isLoading + } + onChange={(newValue: Date | null) => { + storeDiff('discovered', newValue); + }} + /> + + + + + + + + + + + + + + + + + + + {t('View.Galaxy.View.group_3', { defaultValue: 'Matter' })} + + + + + + + + {t('judo.action.open-page', { defaultValue: 'Matter' })} + + + + + + { + const newValue = values.floatValue === undefined ? null : values.floatValue; + if (data.darkMatter !== newValue) { + storeDiff('darkMatter', newValue); + } + }} + InputLabelProps={{ shrink: true }} + InputProps={{ + readOnly: true || !isFormUpdateable(), + startAdornment: ( + + + + ), + }} + /> + + + + { + const newValue = values.floatValue === undefined ? null : values.floatValue; + if (data.intergalacticDust !== newValue) { + storeDiff('intergalacticDust', newValue); + } + }} + InputLabelProps={{ shrink: true }} + InputProps={{ + readOnly: true || !isFormUpdateable(), + startAdornment: ( + + + + ), + }} + /> + + + + { + const newValue = values.floatValue === undefined ? null : values.floatValue; + if (data.interstellarMedium !== newValue) { + storeDiff('interstellarMedium', newValue); + } + }} + InputLabelProps={{ shrink: true }} + InputProps={{ + readOnly: true || !isFormUpdateable(), + startAdornment: ( + + + + ), + }} + /> + + + + + } + menuItems={[ + { + id: 'God/(esm/_5NwrQFyrEeylCdga_wJIBQ)/OperationFormVisualElement', + label: t('View.Galaxy.View.createDarkMatter', { + defaultValue: 'Create Dark Matter', + }) as string, + onClick: actions.createDarkMatterAction + ? async () => { + await actions.createDarkMatterAction!(); + } + : undefined, + startIcon: , + disabled: isLoading || editMode, + }, + { + id: 'God/(esm/_5Nx5YFyrEeylCdga_wJIBQ)/OperationFormVisualElement', + label: t('View.Galaxy.View.createIntergalacticDust', { + defaultValue: 'Create Intergalactic Dust', + }) as string, + onClick: actions.createIntergalacticDustAction + ? async () => { + await actions.createIntergalacticDustAction!(); + } + : undefined, + startIcon: , + disabled: isLoading || editMode, + }, + { + id: 'God/(esm/_5Nx5YVyrEeylCdga_wJIBQ)/OperationFormVisualElement', + label: t('View.Galaxy.View.createInterstellarMedium', { + defaultValue: 'Create Interstellar Medium', + }) as string, + onClick: actions.createInterstellarMediumAction + ? async () => { + await actions.createInterstellarMediumAction!(); + } + : undefined, + startIcon: , + disabled: isLoading || editMode, + }, + ]} + > + {t('View.Galaxy.View.actionGroup', { defaultValue: 'Create Matter' })} + + + + + + + + + + + + + + + + + ); +} diff --git a/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/containers/View/Galaxy/View/ViewGalaxyViewDialogContainer.tsx.snapshot b/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/containers/View/Galaxy/View/ViewGalaxyViewDialogContainer.tsx.snapshot new file mode 100644 index 00000000..66e86cdb --- /dev/null +++ b/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/containers/View/Galaxy/View/ViewGalaxyViewDialogContainer.tsx.snapshot @@ -0,0 +1,187 @@ +////////////////////////////////////////////////////////////////////////////// +// G E N E R A T E D S O U R C E +// -------------------------------- +// Factory expression: #application.pageContainers +// Path expression: 'src/containers/'+#containerPath(#self)+'/'+#containerComponentName(#self)+'DialogContainer.tsx' +// Template name: actor/src/containers/dialog.tsx +// Template file: actor/src/containers/dialog.tsx.hbs + +import LoadingButton from '@mui/lab/LoadingButton'; +import Button from '@mui/material/Button'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import Grid from '@mui/material/Grid'; +import IconButton from '@mui/material/IconButton'; +import { Suspense, lazy } from 'react'; +import type { Dispatch, SetStateAction } from 'react'; +import { useTranslation } from 'react-i18next'; +import { MdiIcon, useJudoNavigation } from '~/components'; +import { useConfirmDialog } from '~/components/dialog'; +import type { ViewGalaxy, ViewGalaxyStored } from '~/services/data-api/model/ViewGalaxy'; +import type { ViewGalaxyQueryCustomizer } from '~/services/data-api/rest/ViewGalaxyQueryCustomizer'; +import { processQueryCustomizer } from '~/utilities'; +import { getValue } from '~/utilities/helper'; + +import type { ViewGalaxyViewActionDefinitions, ViewGalaxyViewDialogActions, ViewGalaxyViewDialogProps } from './types'; + +const ViewGalaxyView = lazy(() => import('~/containers/View/Galaxy/View/ViewGalaxyView')); + +// Name: View::Galaxy::View +export default function ViewGalaxyViewDialog(props: ViewGalaxyViewDialogProps) { + const { openConfirmDialog } = useConfirmDialog(); + + const { t } = useTranslation(); + const { navigate, back } = useJudoNavigation(); + const { + ownerData, + onClose, + actions, + dataPath, + isLoading, + editMode, + refreshCounter, + data, + isFormUpdateable, + isFormDeleteable, + storeDiff, + validation, + setValidation, + submit, + isDraft, + } = props; + const queryCustomizer: ViewGalaxyQueryCustomizer = { + _mask: actions.getMask?.(), + }; + + return ( + <> + + {actions.getPageTitle ? actions.getPageTitle(data) : ''} + theme.palette.grey[500], + }} + > + + + + + + + + + + {!editMode && actions.backAction && ( + + } + onClick={async () => { + await actions.backAction!(); + }} + > + {t('judo.action.back', { defaultValue: 'Back' })} + + + )} + {!isDraft && actions.refreshAction && ( + + } + onClick={async () => { + await actions.refreshAction!(processQueryCustomizer(queryCustomizer)); + }} + > + {t('judo.action.refresh', { defaultValue: 'Refresh' })} + + + )} + {!editMode && (isFormDeleteable() || isDraft) && actions.deleteAction && ( + + } + onClick={async () => { + actions.deleteAction!(); + }} + > + {t('judo.action.delete', { defaultValue: 'Delete' })} + + + )} + {editMode && actions.cancelAction && ( + + } + onClick={async () => { + await actions.cancelAction!(); + }} + > + {t('judo.action.cancel', { defaultValue: 'Cancel' })} + + + )} + {editMode && actions.updateAction && ( + + } + onClick={async () => { + await actions.updateAction!(); + }} + > + + {isDraft + ? t('judo.dialogs.draft.submit', { defaultValue: 'Ok' }) + : t('judo.action.update', { defaultValue: 'Save' })} + + + + )} + + + ); +} diff --git a/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/containers/View/Galaxy/View/ViewGalaxyViewPageContainer.tsx.snapshot b/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/containers/View/Galaxy/View/ViewGalaxyViewPageContainer.tsx.snapshot new file mode 100644 index 00000000..93139649 --- /dev/null +++ b/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/containers/View/Galaxy/View/ViewGalaxyViewPageContainer.tsx.snapshot @@ -0,0 +1,163 @@ +////////////////////////////////////////////////////////////////////////////// +// G E N E R A T E D S O U R C E +// -------------------------------- +// Factory expression: #getPageContainersToGenerate(#application) +// Path expression: 'src/containers/'+#containerPath(#self)+'/'+#containerComponentName(#self)+'PageContainer.tsx' +// Template name: actor/src/containers/page.tsx +// Template file: actor/src/containers/page.tsx.hbs + +import LoadingButton from '@mui/lab/LoadingButton'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Grid from '@mui/material/Grid'; +import { Suspense, lazy } from 'react'; +import type { Dispatch, SetStateAction } from 'react'; +import { useTranslation } from 'react-i18next'; +import { MdiIcon, PageHeader, useJudoNavigation } from '~/components'; +import { useConfirmDialog } from '~/components/dialog'; +import type { ViewGalaxy, ViewGalaxyStored } from '~/services/data-api/model/ViewGalaxy'; +import type { ViewGalaxyQueryCustomizer } from '~/services/data-api/rest/ViewGalaxyQueryCustomizer'; +import { mainContainerPadding } from '~/theme'; +import { processQueryCustomizer } from '~/utilities'; +import { getValue } from '~/utilities/helper'; + +import type { ViewGalaxyViewActionDefinitions, ViewGalaxyViewPageActions, ViewGalaxyViewPageProps } from './types'; + +const ViewGalaxyView = lazy(() => import('~/containers/View/Galaxy/View/ViewGalaxyView')); + +// Name: View::Galaxy::View +export default function ViewGalaxyViewPage(props: ViewGalaxyViewPageProps) { + const { openConfirmDialog } = useConfirmDialog(); + + const { t } = useTranslation(); + const { navigate, back } = useJudoNavigation(); + const { + actions, + dataPath, + isLoading, + editMode, + refreshCounter, + data, + isFormUpdateable, + isFormDeleteable, + storeDiff, + validation, + setValidation, + submit, + } = props; + const isDraft = false; // currently no page can be opened as draft, but we need this variable anyway + const queryCustomizer: ViewGalaxyQueryCustomizer = { + _mask: actions.getMask?.(), + }; + + return ( + <> + + {!editMode && actions.backAction && ( + + } + disabled={isLoading || editMode} + onClick={async () => { + await actions.backAction!(); + }} + > + {t('judo.action.back', { defaultValue: 'Back' })} + + + )} + {!isDraft && actions.refreshAction && ( + + } + disabled={isLoading || editMode} + onClick={async () => { + await actions.refreshAction!(processQueryCustomizer(queryCustomizer)); + }} + > + {t('judo.action.refresh', { defaultValue: 'Refresh' })} + + + )} + {!editMode && (isFormDeleteable() || isDraft) && actions.deleteAction && ( + + } + disabled={isLoading || editMode} + onClick={async () => { + actions.deleteAction!(); + }} + > + {t('judo.action.delete', { defaultValue: 'Delete' })} + + + )} + {editMode && actions.cancelAction && ( + + } + disabled={isLoading} + onClick={async () => { + await actions.cancelAction!(); + }} + > + {t('judo.action.cancel', { defaultValue: 'Cancel' })} + + + )} + {editMode && actions.updateAction && ( + + } + disabled={isLoading} + onClick={async () => { + await actions.updateAction!(); + }} + > + {t('judo.action.update', { defaultValue: 'Save' })} + + + )} +
{/* Placeholder */}
+
+ + + + + + + ); +} diff --git a/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/pages/God/God/Earth/AccessViewPage/index.tsx.snapshot b/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/pages/God/God/Earth/AccessViewPage/index.tsx.snapshot new file mode 100644 index 00000000..60899327 --- /dev/null +++ b/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/pages/God/God/Earth/AccessViewPage/index.tsx.snapshot @@ -0,0 +1,658 @@ +////////////////////////////////////////////////////////////////////////////// +// G E N E R A T E D S O U R C E +// -------------------------------- +// Factory expression: #getPagesForRouting(#application) +// Path expression: 'src/pages/'+#pagePath(#self)+'/index.tsx' +// Template name: actor/src/pages/index.tsx +// Template file: actor/src/pages/index.tsx.hbs + +import type { GridFilterModel } from '@mui/x-data-grid'; +import { OBJECTCLASS } from '@pandino/pandino-api'; +import { useTrackService } from '@pandino/react-hooks'; +import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import type { Dispatch, FC, ReactNode, SetStateAction } from 'react'; +import { useTranslation } from 'react-i18next'; +import { v4 as uuidv4 } from 'uuid'; +import { useJudoNavigation } from '~/components'; +import type { Filter, FilterOption } from '~/components-api'; +import { useConfirmDialog, useFilterDialog } from '~/components/dialog'; +import type { ViewPlanetViewPageActions, ViewPlanetViewPageProps } from '~/containers/View/Planet/View/types'; +import { useViewPlanetViewCreateChaosForInstanceInputForm } from '~/dialogs/View/Planet/View/CreateChaosForInstance/Input/Form/hooks'; +import { useViewPlanetViewCreateCreatureInputForm } from '~/dialogs/View/Planet/View/CreateCreature/Input/Form/hooks'; +import { useViewPlanetViewCreateStaticChaosInputForm } from '~/dialogs/View/Planet/View/CreateStaticChaos/Input/Form/hooks'; +import { useViewPlanetViewGroupActionGroupSendASignRelationTableCallSelector } from '~/dialogs/View/Planet/View/Group/ActionGroup/SendASign/Relation/Table/CallSelector/hooks'; +import { useViewPlanetViewGroup_2RightCreaturesPunishCreatureRelationTableCallSelector } from '~/dialogs/View/Planet/View/Group_2/Right/Creatures/PunishCreature/Relation/Table/CallSelector/hooks'; +import { useViewPlanetViewGroup_2RightCreaturesRewardCreatureRelationTableCallSelector } from '~/dialogs/View/Planet/View/Group_2/Right/Creatures/RewardCreature/Relation/Table/CallSelector/hooks'; +import { useViewPlanetViewGroup_2RightCreaturesTableAddSelectorPage } from '~/dialogs/View/Planet/View/Group_2/Right/Creatures/TableAddSelectorPage/hooks'; +import { useViewPlanetViewGroup_2RightCreaturesTalkToGodRelationTableCallSelector } from '~/dialogs/View/Planet/View/Group_2/Right/Creatures/TalkToGod/Relation/Table/CallSelector/hooks'; +import { useCRUDDialog, useSnacks, useViewData } from '~/hooks'; +import { routeToViewPlanetViewChooseTheMessiahOutputView } from '~/routes'; +import { routeToViewPlanetViewHateGodOutputView } from '~/routes'; +import { routeToViewPlanetViewLoveGodOutputView } from '~/routes'; +import { routeToViewPlanetCreaturesRelationViewPage } from '~/routes'; +import type { JudoIdentifiable } from '~/services/data-api/common/JudoIdentifiable'; +import { draftIdentifierPrefix } from '~/services/data-api/common/utils'; +import type { ViewCreature, ViewCreatureStored } from '~/services/data-api/model/ViewCreature'; +import type { ViewMessage, ViewMessageStored } from '~/services/data-api/model/ViewMessage'; +import type { ViewPlanet, ViewPlanetStored } from '~/services/data-api/model/ViewPlanet'; +import type { ViewCreatureQueryCustomizer } from '~/services/data-api/rest/ViewCreatureQueryCustomizer'; +import type { ViewMessageQueryCustomizer } from '~/services/data-api/rest/ViewMessageQueryCustomizer'; +import type { ViewPlanetQueryCustomizer } from '~/services/data-api/rest/ViewPlanetQueryCustomizer'; +import type { JudoRestResponse } from '~/services/data-api/rest/requestResponse'; +import { GodServiceForEarthImpl } from '~/services/data-axios/GodServiceForEarthImpl'; +import { judoAxiosProvider } from '~/services/data-axios/JudoAxiosProvider'; +import { PageContainerTransition } from '~/theme/animations'; +import { getValue, processQueryCustomizer, setValue, simpleCloneDeep, useErrorHandler } from '~/utilities'; +import type { DialogResult } from '~/utilities'; +import { type ViewPlanetViewViewModel, ViewPlanetViewViewModelContext } from './context'; +import type { ViewPlanetViewActionsHook } from './customization'; +import { GOD_GOD_EARTH_ACCESS_VIEW_PAGE_ACTIONS_HOOK_INTERFACE_KEY } from './customization'; +import type { ViewPlanetViewPageActionsExtended } from './types'; + +const ViewPlanetViewPageContainer = lazy(() => import('~/containers/View/Planet/View/ViewPlanetViewPageContainer')); + +// XMIID: God/(esm/_oaE_YM_fEe6fibzd7gNETg)/AccessViewPageDefinition +// Name: God::God::earth::AccessViewPage +export default function GodGodEarthAccessViewPage() { + const dataPath = ''; + const isDraft = false; + const owner = useRef(null); + + // Services + const godServiceForEarthImpl = useMemo(() => new GodServiceForEarthImpl(judoAxiosProvider), []); + + // Hooks section + const { t } = useTranslation(); + const { showSuccessSnack, showErrorSnack } = useSnacks(); + const { navigate, back: navigateBack } = useJudoNavigation(); + const { openFilterDialog } = useFilterDialog(); + const { openConfirmDialog } = useConfirmDialog(); + const { setLatestViewData, setRouterPageData } = useViewData(); + const handleError = useErrorHandler(); + const openCRUDDialog = useCRUDDialog(); + + // State section + const [isLoading, setIsLoading] = useState(false); + const [editMode, setEditMode] = useState(false); + const [refreshCounter, setRefreshCounter] = useState(0); + const [data, setData] = useState({} as ViewPlanetStored); + const [validation, setValidation] = useState>(new Map()); + + // Ref section + const singletonHost = useRef<{ __signedIdentifier: string }>({} as unknown as { __signedIdentifier: string }); + + // Callback section + const storeDiff: (attributeName: keyof ViewPlanet, value: any) => void = useCallback( + (attributeName: keyof ViewPlanet, value: any) => { + setData((prevData) => ({ + ...prevData, + [attributeName]: value, + })); + if (!editMode) { + setEditMode(true); + } + }, + [data, editMode], + ); + const isFormUpdateable = useCallback(() => { + return true && typeof data?.__updateable === 'boolean' && data?.__updateable; + }, [data]); + const isFormDeleteable = useCallback(() => { + return true && typeof data?.__deleteable === 'boolean' && data?.__deleteable; + }, [data]); + + const getPageQueryCustomizer: () => ViewPlanetQueryCustomizer = () => ({ + _mask: actions.getMask + ? actions.getMask!() + : '{habitable,inhabited,name,peaceful,creatures{id,name,punished,rewarded}}', + }); + + // Masks + const getMask = () => '{habitable,inhabited,name,peaceful,creatures{id,name,punished,rewarded}}'; + const getCreaturesMask = () => '{id,name,punished,rewarded}'; + + // Private actions + const submit = async () => { + await updateAction(); + await createLifeForPlanetAction(); + }; + const refresh = async (forced = false) => { + if (!editMode || forced) { + if (actions.refreshAction) { + await actions.refreshAction!(processQueryCustomizer(getPageQueryCustomizer())); + } + } + }; + const produceDataAdjustedOwner = useCallback(() => { + const copy = simpleCloneDeep(owner.current); + setValue(copy, dataPath, simpleCloneDeep(data)); + return copy; + }, [data, owner]); + + // Validation + const validate: (target: any) => Promise = useCallback( + async (target) => { + await godServiceForEarthImpl.validateUpdate(target); + }, + [data, godServiceForEarthImpl], + ); + + // Pandino Action overrides + const { service: customActionsHook } = useTrackService( + `(${OBJECTCLASS}=${GOD_GOD_EARTH_ACCESS_VIEW_PAGE_ACTIONS_HOOK_INTERFACE_KEY})`, + ); + const customActions: ViewPlanetViewPageActionsExtended | undefined = customActionsHook?.( + data, + editMode, + storeDiff, + refresh, + submit, + ); + + // Dialog hooks + const openViewPlanetViewCreateChaosForInstanceInputForm = useViewPlanetViewCreateChaosForInstanceInputForm(); + const openViewPlanetViewCreateCreatureInputForm = useViewPlanetViewCreateCreatureInputForm(); + const openViewPlanetViewCreateStaticChaosInputForm = useViewPlanetViewCreateStaticChaosInputForm(); + const openViewPlanetViewGroupActionGroupSendASignRelationTableCallSelector = + useViewPlanetViewGroupActionGroupSendASignRelationTableCallSelector(); + const openViewPlanetViewGroup_2RightCreaturesTableAddSelectorPage = + useViewPlanetViewGroup_2RightCreaturesTableAddSelectorPage(); + const openViewPlanetViewGroup_2RightCreaturesPunishCreatureRelationTableCallSelector = + useViewPlanetViewGroup_2RightCreaturesPunishCreatureRelationTableCallSelector(); + const openViewPlanetViewGroup_2RightCreaturesRewardCreatureRelationTableCallSelector = + useViewPlanetViewGroup_2RightCreaturesRewardCreatureRelationTableCallSelector(); + const openViewPlanetViewGroup_2RightCreaturesTalkToGodRelationTableCallSelector = + useViewPlanetViewGroup_2RightCreaturesTalkToGodRelationTableCallSelector(); + + // Action section + const getPageTitle = (data: ViewPlanet): string => { + return data.name as string; + }; + // BackAction: God/(esm/_oaE_YM_fEe6fibzd7gNETg)/AccessViewBackAction + const backAction = async () => { + navigateBack(); + }; + // CancelAction: God/(esm/_oaE_YM_fEe6fibzd7gNETg)/AccessViewCancelAction + const cancelAction = async () => { + setValidation(new Map()); + // no need to set editMode to false, given refresh should do it implicitly + await refresh(true); + }; + // DeleteAction: God/(esm/_oaE_YM_fEe6fibzd7gNETg)/AccessViewDeleteAction + const deleteAction = async () => { + try { + const confirmed = await openConfirmDialog( + 'delete-action', + t('judo.modal.confirm.confirm-delete', { + defaultValue: 'Are you sure you would like to delete the selected element?', + }), + t('judo.modal.confirm.confirm-title', { defaultValue: 'Confirm action' }), + ); + if (confirmed) { + await godServiceForEarthImpl.delete(data); + showSuccessSnack(t('judo.action.delete.success', { defaultValue: 'Delete successful' })); + navigateBack(); + } + } catch (error) { + handleError(error, undefined, data); + } + }; + // RefreshAction: God/(esm/_oaE_YM_fEe6fibzd7gNETg)/AccessViewRefreshAction + const refreshAction = async ( + queryCustomizer: ViewPlanetQueryCustomizer, + ): Promise> => { + try { + setIsLoading(true); + setEditMode(false); + const response = await godServiceForEarthImpl.refresh(owner.current, getPageQueryCustomizer()); + const { data: result } = response; + if (!isDraft) { + owner.current = result; + } + setData(result); + setLatestViewData(result); + setRouterPageData(result); + if (customActions?.postRefreshAction) { + await customActions?.postRefreshAction(result, storeDiff, setValidation); + } + return response; + } catch (error) { + handleError(error); + setLatestViewData(null); + setRouterPageData(null); + return Promise.reject(error); + } finally { + setIsLoading(false); + setRefreshCounter((prevCounter) => prevCounter + 1); + } + }; + // UpdateAction: God/(esm/_oaE_YM_fEe6fibzd7gNETg)/AccessViewUpdateAction + const updateAction = async () => { + setIsLoading(true); + try { + const { data: res } = await godServiceForEarthImpl.update(data); + if (res) { + showSuccessSnack(t('judo.action.save.success', { defaultValue: 'Changes saved' })); + setValidation(new Map()); + setEditMode(false); + await actions.refreshAction!(getPageQueryCustomizer()); + } + } catch (error) { + handleError(error, { setValidation }, data); + } finally { + setIsLoading(false); + } + }; + // ParameterlessCallOperationAction: God/(esm/_bkGTMNAmEe65Ld16OAb1kw)/OperationFormCallAction/(discriminator/God/(esm/_oaE_YM_fEe6fibzd7gNETg)/AccessViewPageDefinition) + const bangForPlanetAction = async (target?: ViewPlanetStored) => { + try { + setIsLoading(true); + await godServiceForEarthImpl.bang(data); + if (customActions?.postBangForPlanetAction) { + await customActions.postBangForPlanetAction(target!); + } else { + showSuccessSnack( + t('judo.action.operation.success', { defaultValue: 'Operation executed successfully' }) as string, + ); + await refresh(); + } + } catch (error) { + handleError(error); + } finally { + setIsLoading(false); + } + }; + // OpenOperationInputFormAction: God/(esm/_sbMMINAmEe65Ld16OAb1kw)/OperationFormCallAction/(discriminator/God/(esm/_oaE_YM_fEe6fibzd7gNETg)/AccessViewPageDefinition) + const createChaosForInstanceAction = async (isDraft?: boolean, ownerValidation?: (target: any) => Promise) => { + const { result, data: returnedData } = await openViewPlanetViewCreateChaosForInstanceInputForm({ + ownerData: data, + }); + if (result === 'submit' && !editMode) { + await refresh(); + } + }; + // OpenOperationInputFormAction: God/(esm/_j_0QYNAoEe65Ld16OAb1kw)/OperationFormCallAction/(discriminator/God/(esm/_oaE_YM_fEe6fibzd7gNETg)/AccessViewPageDefinition) + const createStaticChaosAction = async (isDraft?: boolean, ownerValidation?: (target: any) => Promise) => { + const { result, data: returnedData } = await openViewPlanetViewCreateStaticChaosInputForm({ + ownerData: data, + }); + if (result === 'submit' && !editMode) { + await refresh(); + } + }; + // ParameterlessCallOperationAction: God/(esm/_a1neIFDkEeyjf_w6-3_EHA)/OperationFormCallAction/(discriminator/God/(esm/_oaE_YM_fEe6fibzd7gNETg)/AccessViewPageDefinition) + const endWarForPlanetAction = async (target?: ViewPlanetStored) => { + try { + setIsLoading(true); + await godServiceForEarthImpl.endWar(data); + if (customActions?.postEndWarForPlanetAction) { + await customActions.postEndWarForPlanetAction(target!); + } else { + showSuccessSnack( + t('judo.action.operation.success', { defaultValue: 'Operation executed successfully' }) as string, + ); + await refresh(); + } + } catch (error) { + handleError(error); + } finally { + setIsLoading(false); + } + }; + // OpenOperationInputSelectorAction: God/(esm/_a1osQVDkEeyjf_w6-3_EHA)/OperationFormCallAction/(discriminator/God/(esm/_oaE_YM_fEe6fibzd7gNETg)/AccessViewPageDefinition) + const sendASignAction = async () => { + const { result, data: returnedData } = await openViewPlanetViewGroupActionGroupSendASignRelationTableCallSelector({ + ownerData: data, + }); + if (result === 'submit') { + await refresh(); + } + }; + // ParameterlessCallOperationAction: God/(esm/_a1pTUFDkEeyjf_w6-3_EHA)/OperationFormCallAction/(discriminator/God/(esm/_oaE_YM_fEe6fibzd7gNETg)/AccessViewPageDefinition) + const startWarForPlanetAction = async (target?: ViewPlanetStored) => { + try { + setIsLoading(true); + await godServiceForEarthImpl.startWar(data); + if (customActions?.postStartWarForPlanetAction) { + await customActions.postStartWarForPlanetAction(target!); + } else { + showSuccessSnack( + t('judo.action.operation.success', { defaultValue: 'Operation executed successfully' }) as string, + ); + await refresh(); + } + } catch (error) { + handleError(error); + } finally { + setIsLoading(false); + } + }; + // ParameterlessCallOperationAction: God/(esm/_a1m3EFDkEeyjf_w6-3_EHA)/OperationFormCallAction/(discriminator/God/(esm/_oaE_YM_fEe6fibzd7gNETg)/AccessViewPageDefinition) + const createLifeForPlanetAction = async (target?: ViewPlanetStored) => { + try { + setIsLoading(true); + await godServiceForEarthImpl.createLife(data); + if (customActions?.postCreateLifeForPlanetAction) { + await customActions.postCreateLifeForPlanetAction(target!); + } else { + showSuccessSnack( + t('judo.action.operation.success', { defaultValue: 'Operation executed successfully' }) as string, + ); + await refresh(); + } + } catch (error) { + handleError(error); + } finally { + setIsLoading(false); + } + }; + // ParameterlessCallOperationAction: God/(esm/_a1m3EVDkEeyjf_w6-3_EHA)/OperationFormCallAction/(discriminator/God/(esm/_oaE_YM_fEe6fibzd7gNETg)/AccessViewPageDefinition) + const destroyLifeForPlanetAction = async (target?: ViewPlanetStored) => { + try { + setIsLoading(true); + await godServiceForEarthImpl.destroyLife(data); + if (customActions?.postDestroyLifeForPlanetAction) { + await customActions.postDestroyLifeForPlanetAction(target!); + } else { + showSuccessSnack( + t('judo.action.operation.success', { defaultValue: 'Operation executed successfully' }) as string, + ); + await refresh(); + } + } catch (error) { + handleError(error); + } finally { + setIsLoading(false); + } + }; + // OpenAddSelectorAction: God/(esm/_QKcpkE7vEeycO-gUAWxcVg)/TabularReferenceTableAddSelectorOpenPageAction/(discriminator/God/(esm/_oaE_YM_fEe6fibzd7gNETg)/AccessViewPageDefinition) + const creaturesOpenAddSelectorAction = async () => { + const { result, data: returnedData } = await openViewPlanetViewGroup_2RightCreaturesTableAddSelectorPage({ + ownerData: data, + alreadySelected: data.creatures ?? [], + }); + if (result === 'submit') { + if (Array.isArray(returnedData) && returnedData.length) { + storeDiff('creatures', [...(data.creatures || []), ...returnedData]); + } + } + }; + + // BulkDeleteAction: God/(esm/_QKcpkE7vEeycO-gUAWxcVg)/TabularReferenceTableBulkDeleteAction/(discriminator/God/(esm/_oaE_YM_fEe6fibzd7gNETg)/AccessViewPageDefinition) + const creaturesBulkDeleteAction = async ( + selectedRows: ViewCreatureStored[], + ): Promise>> => { + return new Promise((resolve) => { + const selectedIds = selectedRows.map((r) => r.__identifier); + const newList = (data?.creatures ?? []).filter((c: any) => !selectedIds.includes(c.__identifier)); + storeDiff('creatures', newList); + resolve({ + result: 'delete', + data: [], + }); + }); + }; + // BulkRemoveAction: God/(esm/_QKcpkE7vEeycO-gUAWxcVg)/TabularReferenceTableBulkRemoveAction/(discriminator/God/(esm/_oaE_YM_fEe6fibzd7gNETg)/AccessViewPageDefinition) + const creaturesBulkRemoveAction = async ( + selectedRows: ViewCreatureStored[], + ): Promise>> => { + return new Promise((resolve) => { + const selectedIds = selectedRows.map((r) => r.__identifier); + const newList = (data?.creatures ?? []).filter((c: any) => !selectedIds.includes(c.__identifier)); + storeDiff('creatures', newList); + resolve({ + result: 'submit', + data: [], + }); + }); + }; + // ClearAction: God/(esm/_QKcpkE7vEeycO-gUAWxcVg)/TabularReferenceTableClearAction/(discriminator/God/(esm/_oaE_YM_fEe6fibzd7gNETg)/AccessViewPageDefinition) + const creaturesClearAction = async () => { + storeDiff('creatures', []); + }; + // FilterAction: God/(esm/_QKcpkE7vEeycO-gUAWxcVg)/TabularReferenceTableFilterAction/(discriminator/God/(esm/_oaE_YM_fEe6fibzd7gNETg)/AccessViewPageDefinition) + const creaturesFilterAction = async ( + id: string, + filterOptions: FilterOption[], + model?: GridFilterModel, + filters?: Filter[], + ): Promise<{ model?: GridFilterModel; filters?: Filter[] }> => { + const newFilters = await openFilterDialog(id, filterOptions, filters); + return { + filters: newFilters, + }; + }; + // RowDeleteAction: God/(esm/_QKcpkE7vEeycO-gUAWxcVg)/TabularReferenceTableRowDeleteAction/(discriminator/God/(esm/_oaE_YM_fEe6fibzd7gNETg)/AccessViewPageDefinition) + const creaturesRowDeleteAction = async (target: ViewCreatureStored) => { + try { + const confirmed = await openConfirmDialog( + 'row-delete-action', + t('judo.modal.confirm.confirm-delete', { + defaultValue: 'Are you sure you would like to delete the selected element?', + }), + t('judo.modal.confirm.confirm-title', { defaultValue: 'Confirm action' }), + ); + if (confirmed) { + const newList = (data?.creatures ?? []).filter((c: any) => c.__identifier !== target.__identifier); + storeDiff('creatures', newList); + } + } catch (error) { + handleError(error, undefined, target); + } + }; + // RemoveAction: God/(esm/_QKcpkE7vEeycO-gUAWxcVg)/TabularReferenceTableRowRemoveAction/(discriminator/God/(esm/_oaE_YM_fEe6fibzd7gNETg)/AccessViewPageDefinition) + const creaturesRemoveAction = async (target: ViewCreatureStored) => { + const newList = (data?.creatures ?? []).filter((c: any) => c.__identifier !== target.__identifier); + storeDiff('creatures', newList); + }; + // OpenPageAction: God/(esm/_QKcpkE7vEeycO-gUAWxcVg)/TabularReferenceTableRowViewAction/(discriminator/God/(esm/_oaE_YM_fEe6fibzd7gNETg)/AccessViewPageDefinition) + const creaturesOpenPageAction = async (target: ViewCreatureStored, isDraftParam?: boolean) => { + const itemIndex = (data.creatures || []).findIndex((r) => r.__identifier === target.__identifier)!; + // if the `target` is missing we are likely navigating to a relation table page, in which case we need the owner's id + navigate(routeToViewPlanetCreaturesRelationViewPage(((target as ViewCreatureStored) || data).__signedIdentifier)); + }; + // ParameterlessCallOperationAction: God/(esm/_6l6P0TV7Ee-rMcMaep1uNQ)/OperationFormTableTableCallAction/(discriminator/God/(esm/_QKcpkE7vEeycO-gUAWxcVg)/TabularReferenceField) + const creaturesChooseTheMessiahForPlanetAction = async (target?: ViewPlanetStored) => { + try { + setIsLoading(true); + const { data: result } = await godServiceForEarthImpl.chooseTheMessiah(data); + if (customActions?.postCreaturesChooseTheMessiahForPlanetAction) { + await customActions.postCreaturesChooseTheMessiahForPlanetAction(target!, result); + } else { + showSuccessSnack( + t('judo.action.operation.success', { defaultValue: 'Operation executed successfully' }) as string, + ); + + navigate(routeToViewPlanetViewChooseTheMessiahOutputView(result.__signedIdentifier)); + } + } catch (error) { + handleError(error); + } finally { + setIsLoading(false); + } + }; + // OpenOperationInputFormAction: God/(esm/_R_BrUTV8Ee-rMcMaep1uNQ)/OperationFormTableTableCallAction/(discriminator/God/(esm/_QKcpkE7vEeycO-gUAWxcVg)/TabularReferenceField) + const creaturesCreateCreatureAction = async ( + target: ViewPlanetStored, + isDraft?: boolean, + ownerValidation?: (target: any) => Promise, + ) => { + const { result, data: returnedData } = await openViewPlanetViewCreateCreatureInputForm({ + ownerData: data, + }); + if (result === 'submit' && !editMode) { + await refresh(); + } + }; + // ParameterlessCallOperationAction: God/(esm/_3_lGwTV7Ee-rMcMaep1uNQ)/OperationFormTableRowCallAction/(discriminator/God/(esm/_QKcpkE7vEeycO-gUAWxcVg)/TabularReferenceField) + const creaturesHateGodForCreatureAction = async (target?: ViewCreatureStored) => { + try { + setIsLoading(true); + const { data: result } = await godServiceForEarthImpl.hateGodForCreatures(target!); + if (customActions?.postCreaturesHateGodForCreatureAction) { + await customActions.postCreaturesHateGodForCreatureAction(target!, result); + } else { + showSuccessSnack( + t('judo.action.operation.success', { defaultValue: 'Operation executed successfully' }) as string, + ); + + navigate(routeToViewPlanetViewHateGodOutputView(result.__signedIdentifier)); + } + } catch (error) { + handleError(error); + } finally { + setIsLoading(false); + } + }; + // ParameterlessCallOperationAction: God/(esm/_3_lGwzV7Ee-rMcMaep1uNQ)/OperationFormTableRowCallAction/(discriminator/God/(esm/_QKcpkE7vEeycO-gUAWxcVg)/TabularReferenceField) + const creaturesLoveGodForCreatureAction = async (target?: ViewCreatureStored) => { + try { + setIsLoading(true); + const { data: result } = await godServiceForEarthImpl.loveGodForCreatures(target!); + if (customActions?.postCreaturesLoveGodForCreatureAction) { + await customActions.postCreaturesLoveGodForCreatureAction(target!, result); + } else { + showSuccessSnack( + t('judo.action.operation.success', { defaultValue: 'Operation executed successfully' }) as string, + ); + + navigate(routeToViewPlanetViewLoveGodOutputView(result.__signedIdentifier)); + } + } catch (error) { + handleError(error); + } finally { + setIsLoading(false); + } + }; + // OpenOperationInputSelectorAction: God/(esm/_ASgmQTV8Ee-rMcMaep1uNQ)/OperationFormTableTableCallAction/(discriminator/God/(esm/_QKcpkE7vEeycO-gUAWxcVg)/TabularReferenceField) + const creaturesPunishCreatureAction = async (target?: ViewPlanetStored) => { + const { result, data: returnedData } = + await openViewPlanetViewGroup_2RightCreaturesPunishCreatureRelationTableCallSelector({ + ownerData: data, + }); + if (result === 'submit') { + await refresh(); + } + }; + // OpenOperationInputSelectorAction: God/(esm/_AShNUTV8Ee-rMcMaep1uNQ)/OperationFormTableTableCallAction/(discriminator/God/(esm/_QKcpkE7vEeycO-gUAWxcVg)/TabularReferenceField) + const creaturesRewardCreatureAction = async (target?: ViewPlanetStored) => { + const { result, data: returnedData } = + await openViewPlanetViewGroup_2RightCreaturesRewardCreatureRelationTableCallSelector({ + ownerData: data, + }); + if (result === 'submit') { + await refresh(); + } + }; + // OpenOperationInputSelectorAction: God/(esm/_3_lGxTV7Ee-rMcMaep1uNQ)/OperationFormTableRowCallAction/(discriminator/God/(esm/_QKcpkE7vEeycO-gUAWxcVg)/TabularReferenceField) + const creaturesTalkToGodAction = async (target?: ViewPlanetStored) => { + const { result, data: returnedData } = + await openViewPlanetViewGroup_2RightCreaturesTalkToGodRelationTableCallSelector({ + ownerData: target!, + }); + if (result === 'submit') { + await refresh(); + } + }; + const getSingletonPayload = async (): Promise> => { + const { data: sp } = await godServiceForEarthImpl.refreshForEarth({ + _mask: '{}', + }); + return sp; + }; + + const actions: ViewPlanetViewPageActions = { + getPageTitle, + backAction, + cancelAction, + deleteAction, + refreshAction, + updateAction, + bangForPlanetAction, + createChaosForInstanceAction, + createStaticChaosAction, + endWarForPlanetAction, + sendASignAction, + startWarForPlanetAction, + createLifeForPlanetAction, + destroyLifeForPlanetAction, + creaturesOpenAddSelectorAction, + creaturesBulkDeleteAction, + creaturesBulkRemoveAction, + creaturesClearAction, + creaturesFilterAction, + creaturesRowDeleteAction, + creaturesRemoveAction, + creaturesOpenPageAction, + creaturesChooseTheMessiahForPlanetAction, + creaturesCreateCreatureAction, + creaturesHateGodForCreatureAction, + creaturesLoveGodForCreatureAction, + creaturesPunishCreatureAction, + creaturesRewardCreatureAction, + creaturesTalkToGodAction, + getMask, + getCreaturesMask, + ...(customActions ?? {}), + }; + + // ViewModel setup + const viewModel: ViewPlanetViewViewModel = { + actions, + isLoading, + setIsLoading, + refreshCounter, + editMode, + setEditMode, + refresh, + data, + validation, + setValidation, + storeDiff, + submit, + isFormUpdateable, + isFormDeleteable, + }; + + // Effect section + useEffect(() => { + (async () => { + const res = await getSingletonPayload(); + if (res?.__signedIdentifier) { + singletonHost.current = res; + owner.current = res; + } else { + navigate('*'); + return; + } + await actions.refreshAction!(getPageQueryCustomizer()); + })(); + }, []); + + return ( + + +
+ + + + + + ); +} diff --git a/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/pages/God/God/Galaxies/AccessTablePage/index.tsx.snapshot b/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/pages/God/God/Galaxies/AccessTablePage/index.tsx.snapshot new file mode 100644 index 00000000..217f20e8 --- /dev/null +++ b/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/pages/God/God/Galaxies/AccessTablePage/index.tsx.snapshot @@ -0,0 +1,299 @@ +////////////////////////////////////////////////////////////////////////////// +// G E N E R A T E D S O U R C E +// -------------------------------- +// Factory expression: #getPagesForRouting(#application) +// Path expression: 'src/pages/'+#pagePath(#self)+'/index.tsx' +// Template name: actor/src/pages/index.tsx +// Template file: actor/src/pages/index.tsx.hbs + +import type { GridFilterModel } from '@mui/x-data-grid'; +import { OBJECTCLASS } from '@pandino/pandino-api'; +import { useTrackService } from '@pandino/react-hooks'; +import { Suspense, lazy, useCallback, useMemo, useRef, useState } from 'react'; +import type { Dispatch, FC, ReactNode, SetStateAction } from 'react'; +import { useTranslation } from 'react-i18next'; +import { v4 as uuidv4 } from 'uuid'; +import { useJudoNavigation } from '~/components'; +import type { Filter, FilterOption } from '~/components-api'; +import { useConfirmDialog, useFilterDialog } from '~/components/dialog'; +import type { ViewGalaxyTablePageActions, ViewGalaxyTablePageProps } from '~/containers/View/Galaxy/Table/types'; +import { useGodGodGalaxiesAccessFormPage } from '~/dialogs/God/God/Galaxies/AccessFormPage/hooks'; +import { useViewGalaxyTableCreateDarkMatterInputForm } from '~/dialogs/View/Galaxy/Table/CreateDarkMatter/Input/Form/hooks'; +import { useViewGalaxyTableCreateIntergalacticDustInputForm } from '~/dialogs/View/Galaxy/Table/CreateIntergalacticDust/Input/Form/hooks'; +import { useViewGalaxyTableCreateInterstellarMediumInputForm } from '~/dialogs/View/Galaxy/Table/CreateInterstellarMedium/Input/Form/hooks'; +import { useCRUDDialog, useSnacks, useViewData } from '~/hooks'; +import { routeToGodGodGalaxiesAccessViewPage } from '~/routes'; +import type { JudoIdentifiable } from '~/services/data-api/common/JudoIdentifiable'; +import { draftIdentifierPrefix } from '~/services/data-api/common/utils'; +import type { ViewGalaxy, ViewGalaxyStored } from '~/services/data-api/model/ViewGalaxy'; +import type { ViewGalaxyQueryCustomizer } from '~/services/data-api/rest/ViewGalaxyQueryCustomizer'; +import type { JudoRestResponse } from '~/services/data-api/rest/requestResponse'; +import { GodServiceForGalaxiesImpl } from '~/services/data-axios/GodServiceForGalaxiesImpl'; +import { judoAxiosProvider } from '~/services/data-axios/JudoAxiosProvider'; +import { PageContainerTransition } from '~/theme/animations'; +import { getValue, processQueryCustomizer, setValue, simpleCloneDeep, useErrorHandler } from '~/utilities'; +import type { DialogResult } from '~/utilities'; +import { type ViewGalaxyTableViewModel, ViewGalaxyTableViewModelContext } from './context'; +import type { ViewGalaxyTableActionsHook } from './customization'; +import { GOD_GOD_GALAXIES_ACCESS_TABLE_PAGE_ACTIONS_HOOK_INTERFACE_KEY } from './customization'; +import type { ViewGalaxyTablePageActionsExtended } from './types'; + +const ViewGalaxyTablePageContainer = lazy(() => import('~/containers/View/Galaxy/Table/ViewGalaxyTablePageContainer')); + +// XMIID: God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessTablePageDefinition +// Name: God::God::galaxies::AccessTablePage +export default function GodGodGalaxiesAccessTablePage() { + const dataPath = ''; + const isDraft = false; + const owner = useRef(null); + + // Services + const godServiceForGalaxiesImpl = useMemo(() => new GodServiceForGalaxiesImpl(judoAxiosProvider), []); + + // Hooks section + const { t } = useTranslation(); + const { showSuccessSnack, showErrorSnack } = useSnacks(); + const { navigate, back: navigateBack } = useJudoNavigation(); + const { openFilterDialog } = useFilterDialog(); + const { openConfirmDialog } = useConfirmDialog(); + const { setLatestViewData, setRouterPageData } = useViewData(); + const handleError = useErrorHandler(); + const openCRUDDialog = useCRUDDialog(); + + // State section + const [isLoading, setIsLoading] = useState(false); + const [editMode, setEditMode] = useState(false); + const [refreshCounter, setRefreshCounter] = useState(0); + const [data, setData] = useState([]); + + // Masks + const getMask: () => string = () => { + return '{constellation,darkMatter,discovered,intergalacticDust,interstellarMedium,magnitude,nakedEye,name,real}'; + }; + + // Private actions + const submit = async () => {}; + const refresh = async (forced = false) => { + setRefreshCounter((prev) => prev + 1); + }; + const produceDataAdjustedOwner = useCallback(() => { + const copy = simpleCloneDeep(owner.current); + setValue(copy, dataPath, simpleCloneDeep(data)); + return copy; + }, [data, owner]); + + // Validation + + // Pandino Action overrides + const { service: customActionsHook } = useTrackService( + `(${OBJECTCLASS}=${GOD_GOD_GALAXIES_ACCESS_TABLE_PAGE_ACTIONS_HOOK_INTERFACE_KEY})`, + ); + const customActions: ViewGalaxyTablePageActionsExtended | undefined = customActionsHook?.(data, editMode); + + // Dialog hooks + const openGodGodGalaxiesAccessFormPage = useGodGodGalaxiesAccessFormPage(); + const openViewGalaxyTableCreateDarkMatterInputForm = useViewGalaxyTableCreateDarkMatterInputForm(); + const openViewGalaxyTableCreateIntergalacticDustInputForm = useViewGalaxyTableCreateIntergalacticDustInputForm(); + const openViewGalaxyTableCreateInterstellarMediumInputForm = useViewGalaxyTableCreateInterstellarMediumInputForm(); + + // Action section + const getPageTitle = (): string => { + return t('View.Galaxy.Table', { defaultValue: 'Galaxies' }); + }; + // BackAction: God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessTableBackAction + const backAction = async () => { + navigateBack(); + }; + // BulkDeleteAction: God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessTableBulkDeleteAction + const bulkDeleteAction = async (selectedRows: ViewGalaxyStored[]): Promise>> => { + return new Promise((resolve) => { + openCRUDDialog({ + dialogTitle: t('judo.action.bulk-delete', { defaultValue: 'Delete' }), + itemTitleFn: (item) => (actions?.getRowRepresentation ? actions.getRowRepresentation(item) : item.name!), + selectedItems: selectedRows, + action: async (item, successHandler: () => void, errorHandler: (error: any) => void) => { + try { + await godServiceForGalaxiesImpl.delete(item); + successHandler(); + } catch (error) { + errorHandler(error); + } + }, + autoCloseOnSuccess: true, + onClose: async (needsRefresh) => { + if (needsRefresh) { + setRefreshCounter((prev) => prev + 1); + resolve({ + result: 'delete', + data: [], + }); + } else { + resolve({ + result: 'close', + data: [], + }); + } + }, + }); + }); + }; + // OpenCreateFormAction: God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessTableCreateAction + const openCreateFormAction = async () => { + const { + result, + data: returnedData, + openCreated, + } = await openGodGodGalaxiesAccessFormPage({ + ownerData: produceDataAdjustedOwner(), + isDraft: false, + dataPath: `${dataPath ? dataPath + '.' : ''}`, + }); + if (result === 'submit') { + setRefreshCounter((prev) => prev + 1); + } + if (openCreated && returnedData) { + await openPageAction(returnedData!); + } + }; + // RowDeleteAction: God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessTableRowDeleteAction + const rowDeleteAction = async (target: ViewGalaxyStored) => { + try { + const confirmed = await openConfirmDialog( + 'row-delete-action', + t('judo.modal.confirm.confirm-delete', { + defaultValue: 'Are you sure you would like to delete the selected element?', + }), + t('judo.modal.confirm.confirm-title', { defaultValue: 'Confirm action' }), + ); + if (confirmed) { + await godServiceForGalaxiesImpl.delete(target); + showSuccessSnack(t('judo.action.delete.success', { defaultValue: 'Delete successful' })); + setRefreshCounter((prev) => prev + 1); + } + } catch (error) { + handleError(error, undefined, target); + } + }; + // FilterAction: God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessTableTableFilterAction + const filterAction = async ( + id: string, + filterOptions: FilterOption[], + model?: GridFilterModel, + filters?: Filter[], + ): Promise<{ model?: GridFilterModel; filters?: Filter[] }> => { + const newFilters = await openFilterDialog(id, filterOptions, filters); + return { + filters: newFilters, + }; + }; + // RefreshAction: God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessTableTableRefreshAction + const refreshAction = async ( + queryCustomizer: ViewGalaxyQueryCustomizer, + ): Promise> => { + try { + setIsLoading(true); + setEditMode(false); + return godServiceForGalaxiesImpl.list(undefined, queryCustomizer); + } catch (error) { + handleError(error); + setLatestViewData(null); + setRouterPageData(null); + return Promise.reject(error); + } finally { + setIsLoading(false); + } + }; + // OpenPageAction: God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessTableRowViewAction + const openPageAction = async (target: ViewGalaxyStored, isDraftParam?: boolean) => { + // if the `target` is missing we are likely navigating to a relation table page, in which case we need the owner's id + navigate(routeToGodGodGalaxiesAccessViewPage((target as ViewGalaxyStored)!.__signedIdentifier)); + }; + // OpenOperationInputFormAction: God/(esm/_uqp7kDV3Ee-rMcMaep1uNQ)/OperationFormTableRowCallAction/(discriminator/God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessTablePageDefinition) + const createDarkMatterAction = async ( + target: ViewGalaxyStored, + isDraft?: boolean, + ownerValidation?: (target: any) => Promise, + ) => { + const { result, data: returnedData } = await openViewGalaxyTableCreateDarkMatterInputForm({ + ownerData: target, + }); + if (result === 'submit') { + await refresh(); + } + }; + // OpenOperationInputFormAction: God/(esm/_uqqioTV3Ee-rMcMaep1uNQ)/OperationFormTableRowCallAction/(discriminator/God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessTablePageDefinition) + const createIntergalacticDustAction = async ( + target: ViewGalaxyStored, + isDraft?: boolean, + ownerValidation?: (target: any) => Promise, + ) => { + const { result, data: returnedData } = await openViewGalaxyTableCreateIntergalacticDustInputForm({ + ownerData: target, + }); + if (result === 'submit') { + await refresh(); + } + }; + // OpenOperationInputFormAction: God/(esm/_uqqiozV3Ee-rMcMaep1uNQ)/OperationFormTableRowCallAction/(discriminator/God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessTablePageDefinition) + const createInterstellarMediumAction = async ( + target: ViewGalaxyStored, + isDraft?: boolean, + ownerValidation?: (target: any) => Promise, + ) => { + const { result, data: returnedData } = await openViewGalaxyTableCreateInterstellarMediumInputForm({ + ownerData: target, + }); + if (result === 'submit') { + await refresh(); + } + }; + + const actions: ViewGalaxyTablePageActions = { + getPageTitle, + backAction, + bulkDeleteAction, + openCreateFormAction, + rowDeleteAction, + filterAction, + refreshAction, + openPageAction, + createDarkMatterAction, + createIntergalacticDustAction, + createInterstellarMediumAction, + getMask, + ...(customActions ?? {}), + }; + + // ViewModel setup + const viewModel: ViewGalaxyTableViewModel = { + actions, + isLoading, + setIsLoading, + refreshCounter, + editMode, + setEditMode, + refresh, + }; + + // Effect section + + return ( + + +
+ + + + + + ); +} diff --git a/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/pages/God/God/Galaxies/AccessViewPage/index.tsx.snapshot b/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/pages/God/God/Galaxies/AccessViewPage/index.tsx.snapshot new file mode 100644 index 00000000..10dfc5c8 --- /dev/null +++ b/judo-ui-react-itest/ActionGroupTest/action_group_test__god/src/test/resources/snapshots/frontend-react/src/pages/God/God/Galaxies/AccessViewPage/index.tsx.snapshot @@ -0,0 +1,603 @@ +////////////////////////////////////////////////////////////////////////////// +// G E N E R A T E D S O U R C E +// -------------------------------- +// Factory expression: #getPagesForRouting(#application) +// Path expression: 'src/pages/'+#pagePath(#self)+'/index.tsx' +// Template name: actor/src/pages/index.tsx +// Template file: actor/src/pages/index.tsx.hbs + +import type { GridFilterModel } from '@mui/x-data-grid'; +import { OBJECTCLASS } from '@pandino/pandino-api'; +import { useTrackService } from '@pandino/react-hooks'; +import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import type { Dispatch, FC, ReactNode, SetStateAction } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; +import { v4 as uuidv4 } from 'uuid'; +import { useJudoNavigation } from '~/components'; +import type { Filter, FilterOption } from '~/components-api'; +import { useConfirmDialog, useFilterDialog } from '~/components/dialog'; +import type { ViewGalaxyViewPageActions, ViewGalaxyViewPageProps } from '~/containers/View/Galaxy/View/types'; +import { useViewGalaxyAstronomerRelationFormPage } from '~/dialogs/View/Galaxy/Astronomer/RelationFormPage/hooks'; +import { useViewGalaxyStarsRelationFormPage } from '~/dialogs/View/Galaxy/Stars/RelationFormPage/hooks'; +import { useViewGalaxyStarsRelationViewPage } from '~/dialogs/View/Galaxy/Stars/RelationViewPage/hooks'; +import { useViewGalaxyViewCreateDarkMatterInputForm } from '~/dialogs/View/Galaxy/View/CreateDarkMatter/Input/Form/hooks'; +import { useViewGalaxyViewCreateIntergalacticDustInputForm } from '~/dialogs/View/Galaxy/View/CreateIntergalacticDust/Input/Form/hooks'; +import { useViewGalaxyViewCreateInterstellarMediumInputForm } from '~/dialogs/View/Galaxy/View/CreateInterstellarMedium/Input/Form/hooks'; +import { useViewGalaxyViewGroupDiscovererAstronomerLinkSetSelectorPage } from '~/dialogs/View/Galaxy/View/Group/Discoverer/Astronomer/LinkSetSelectorPage/hooks'; +import { useCRUDDialog, useSnacks, useViewData } from '~/hooks'; +import { routeToViewGalaxyAstronomerRelationViewPage } from '~/routes'; +import { routeToViewGalaxyMatterRelationTablePage } from '~/routes'; +import type { JudoIdentifiable } from '~/services/data-api/common/JudoIdentifiable'; +import { draftIdentifierPrefix } from '~/services/data-api/common/utils'; +import type { ViewAstronomer, ViewAstronomerStored } from '~/services/data-api/model/ViewAstronomer'; +import type { ViewGalaxy, ViewGalaxyStored } from '~/services/data-api/model/ViewGalaxy'; +import type { ViewMatter, ViewMatterStored } from '~/services/data-api/model/ViewMatter'; +import type { ViewStar, ViewStarStored } from '~/services/data-api/model/ViewStar'; +import type { ViewAstronomerQueryCustomizer } from '~/services/data-api/rest/ViewAstronomerQueryCustomizer'; +import type { ViewGalaxyQueryCustomizer } from '~/services/data-api/rest/ViewGalaxyQueryCustomizer'; +import type { ViewMatterQueryCustomizer } from '~/services/data-api/rest/ViewMatterQueryCustomizer'; +import type { ViewStarQueryCustomizer } from '~/services/data-api/rest/ViewStarQueryCustomizer'; +import type { JudoRestResponse } from '~/services/data-api/rest/requestResponse'; +import { GodServiceForGalaxiesImpl } from '~/services/data-axios/GodServiceForGalaxiesImpl'; +import { judoAxiosProvider } from '~/services/data-axios/JudoAxiosProvider'; +import { PageContainerTransition } from '~/theme/animations'; +import { + fileHandling, + getValue, + processQueryCustomizer, + setValue, + simpleCloneDeep, + useErrorHandler, +} from '~/utilities'; +import type { DialogResult } from '~/utilities'; +import { type ViewGalaxyViewViewModel, ViewGalaxyViewViewModelContext } from './context'; +import type { ViewGalaxyViewActionsHook } from './customization'; +import { GOD_GOD_GALAXIES_ACCESS_VIEW_PAGE_ACTIONS_HOOK_INTERFACE_KEY } from './customization'; +import type { ViewGalaxyViewPageActionsExtended } from './types'; + +const ViewGalaxyViewPageContainer = lazy(() => import('~/containers/View/Galaxy/View/ViewGalaxyViewPageContainer')); + +// XMIID: God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessViewPageDefinition +// Name: God::God::galaxies::AccessViewPage +export default function GodGodGalaxiesAccessViewPage() { + const dataPath = ''; + const isDraft = false; + const owner = useRef(null); + + // Router params section + const { signedIdentifier } = useParams(); + + // Services + const godServiceForGalaxiesImpl = useMemo(() => new GodServiceForGalaxiesImpl(judoAxiosProvider), []); + + // Hooks section + const { t } = useTranslation(); + const { showSuccessSnack, showErrorSnack } = useSnacks(); + const { navigate, back: navigateBack } = useJudoNavigation(); + const { openFilterDialog } = useFilterDialog(); + const { openConfirmDialog } = useConfirmDialog(); + const { setLatestViewData, setRouterPageData } = useViewData(); + const handleError = useErrorHandler(); + const openCRUDDialog = useCRUDDialog(); + const { exportFile } = fileHandling(); + + // State section + const [isLoading, setIsLoading] = useState(false); + const [editMode, setEditMode] = useState(false); + const [refreshCounter, setRefreshCounter] = useState(0); + const [data, setData] = useState({ __signedIdentifier: signedIdentifier } as ViewGalaxyStored); + const [validation, setValidation] = useState>(new Map()); + + // Ref section + + // Callback section + const storeDiff: (attributeName: keyof ViewGalaxy, value: any) => void = useCallback( + (attributeName: keyof ViewGalaxy, value: any) => { + setData((prevData) => ({ + ...prevData, + [attributeName]: value, + })); + if (!editMode) { + setEditMode(true); + } + }, + [data, editMode], + ); + const isFormUpdateable = useCallback(() => { + return true && typeof data?.__updateable === 'boolean' && data?.__updateable; + }, [data]); + const isFormDeleteable = useCallback(() => { + return true && typeof data?.__deleteable === 'boolean' && data?.__deleteable; + }, [data]); + + const getPageQueryCustomizer: () => ViewGalaxyQueryCustomizer = () => ({ + _mask: actions.getMask + ? actions.getMask!() + : '{constellation,darkMatter,discovered,intergalacticDust,interstellarMedium,magnitude,nakedEye,name,originOfName,real,astronomer{born,name,derivedMessage{message},messages{message},singleMessage{message}},stars{name,lastObservation{date,name},observations{date,name},planets{habitable,inhabited,name,peaceful,creatures{id,name}}}}', + }); + + // Masks + const getMask = () => + '{constellation,darkMatter,discovered,intergalacticDust,interstellarMedium,magnitude,nakedEye,name,originOfName,real,astronomer{born,name,derivedMessage{message},messages{message},singleMessage{message}},stars{name,lastObservation{date,name},observations{date,name},planets{habitable,inhabited,name,peaceful,creatures{id,name}}}}'; + const getAstronomerMask = () => '{born,name,derivedMessage{message},messages{message},singleMessage{message}}'; + const getStarsMask = () => + '{name,lastObservation{date,name},observations{date,name},planets{habitable,inhabited,name,peaceful,creatures{id,name,punished,rewarded}}}'; + + // Private actions + const submit = async () => { + await updateAction(); + }; + const refresh = async (forced = false) => { + if (!editMode || forced) { + if (actions.refreshAction) { + await actions.refreshAction!(processQueryCustomizer(getPageQueryCustomizer())); + } + } + }; + const produceDataAdjustedOwner = useCallback(() => { + const copy = simpleCloneDeep(owner.current); + setValue(copy, dataPath, simpleCloneDeep(data)); + return copy; + }, [data, owner]); + + // Validation + const validate: (target: any) => Promise = useCallback( + async (target) => { + await godServiceForGalaxiesImpl.validateUpdate(target); + }, + [data, godServiceForGalaxiesImpl], + ); + + // Pandino Action overrides + const { service: customActionsHook } = useTrackService( + `(${OBJECTCLASS}=${GOD_GOD_GALAXIES_ACCESS_VIEW_PAGE_ACTIONS_HOOK_INTERFACE_KEY})`, + ); + const customActions: ViewGalaxyViewPageActionsExtended | undefined = customActionsHook?.( + data, + editMode, + storeDiff, + refresh, + submit, + ); + + // Dialog hooks + const openViewGalaxyViewCreateDarkMatterInputForm = useViewGalaxyViewCreateDarkMatterInputForm(); + const openViewGalaxyViewCreateIntergalacticDustInputForm = useViewGalaxyViewCreateIntergalacticDustInputForm(); + const openViewGalaxyViewCreateInterstellarMediumInputForm = useViewGalaxyViewCreateInterstellarMediumInputForm(); + const openViewGalaxyViewGroupDiscovererAstronomerLinkSetSelectorPage = + useViewGalaxyViewGroupDiscovererAstronomerLinkSetSelectorPage(); + const openViewGalaxyAstronomerRelationFormPage = useViewGalaxyAstronomerRelationFormPage(); + const openViewGalaxyStarsRelationFormPage = useViewGalaxyStarsRelationFormPage(); + const openViewGalaxyStarsRelationViewPage = useViewGalaxyStarsRelationViewPage(); + + // Action section + const getPageTitle = (data: ViewGalaxy): string => { + return data.name as string; + }; + // BackAction: God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessViewBackAction + const backAction = async () => { + navigateBack(); + }; + // CancelAction: God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessViewCancelAction + const cancelAction = async () => { + setValidation(new Map()); + // no need to set editMode to false, given refresh should do it implicitly + await refresh(true); + }; + // DeleteAction: God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessViewDeleteAction + const deleteAction = async () => { + try { + const confirmed = await openConfirmDialog( + 'delete-action', + t('judo.modal.confirm.confirm-delete', { + defaultValue: 'Are you sure you would like to delete the selected element?', + }), + t('judo.modal.confirm.confirm-title', { defaultValue: 'Confirm action' }), + ); + if (confirmed) { + await godServiceForGalaxiesImpl.delete(data); + showSuccessSnack(t('judo.action.delete.success', { defaultValue: 'Delete successful' })); + navigateBack(); + } + } catch (error) { + handleError(error, undefined, data); + } + }; + // RefreshAction: God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessViewRefreshAction + const refreshAction = async ( + queryCustomizer: ViewGalaxyQueryCustomizer, + ): Promise> => { + try { + setIsLoading(true); + setEditMode(false); + const response = await godServiceForGalaxiesImpl.refresh( + { __signedIdentifier: signedIdentifier } as any, + getPageQueryCustomizer(), + ); + const { data: result } = response; + if (!isDraft) { + owner.current = result; + } + setData(result); + setLatestViewData(result); + setRouterPageData(result); + if (customActions?.postRefreshAction) { + await customActions?.postRefreshAction(result, storeDiff, setValidation); + } + return response; + } catch (error) { + handleError(error); + setLatestViewData(null); + setRouterPageData(null); + return Promise.reject(error); + } finally { + setIsLoading(false); + setRefreshCounter((prevCounter) => prevCounter + 1); + } + }; + // UpdateAction: God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessViewUpdateAction + const updateAction = async () => { + setIsLoading(true); + try { + const { data: res } = await godServiceForGalaxiesImpl.update(data); + if (res) { + showSuccessSnack(t('judo.action.save.success', { defaultValue: 'Changes saved' })); + setValidation(new Map()); + setEditMode(false); + await actions.refreshAction!(getPageQueryCustomizer()); + } + } catch (error) { + handleError(error, { setValidation }, data); + } finally { + setIsLoading(false); + } + }; + // AutocompleteRangeAction: God/(esm/_conRARMcEe2_DOUDKkB20Q)/TabularReferenceFieldLinkAutocompleteRangeAction/(discriminator/God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessViewPageDefinition) + const astronomerAutocompleteRangeAction = async ( + queryCustomizer: ViewAstronomerQueryCustomizer, + ): Promise => { + try { + const { data: result } = await godServiceForGalaxiesImpl.getRangeForAstronomer(data, queryCustomizer); + return result; + } catch (error: any) { + handleError(error); + return Promise.resolve([]); + } + }; + // AutocompleteSetAction: God/(esm/_conRARMcEe2_DOUDKkB20Q)/TabularReferenceFieldLinkAutocompleteSetAction/(discriminator/God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessViewPageDefinition) + const astronomerAutocompleteSetAction = async (selected: ViewAstronomerStored) => { + try { + storeDiff('astronomer', selected); + } catch (error) { + handleError(error); + return Promise.reject(error); + } + }; + // OpenCreateFormAction: God/(esm/_conRARMcEe2_DOUDKkB20Q)/TabularReferenceFieldLinkCreateAction/(discriminator/God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessViewPageDefinition) + const astronomerOpenCreateFormAction = async () => { + const { + result, + data: returnedData, + openCreated, + } = await openViewGalaxyAstronomerRelationFormPage({ + ownerData: produceDataAdjustedOwner(), + ownerValidation: validate, + isDraft: true, + dataPath: `${dataPath ? dataPath + '.' : ''}astronomer`, + }); + if (result === 'submit' && !editMode) { + await actions.refreshAction!(processQueryCustomizer(getPageQueryCustomizer())); + } else if (result === 'submit-draft' && returnedData) { + const decoratedData = { + ...returnedData, + __identifier: `${draftIdentifierPrefix}${uuidv4()}`, + }; + const newData = decoratedData; + storeDiff('astronomer', newData); + return; + } + if (openCreated && returnedData) { + await astronomerOpenPageAction(returnedData!); + } + }; + // RowDeleteAction: God/(esm/_conRARMcEe2_DOUDKkB20Q)/TabularReferenceFieldLinkDeleteAction/(discriminator/God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessViewPageDefinition) + const astronomerRowDeleteAction = async (target: ViewAstronomerStored) => { + try { + const confirmed = await openConfirmDialog( + 'row-delete-action', + t('judo.modal.confirm.confirm-delete', { + defaultValue: 'Are you sure you would like to delete the selected element?', + }), + t('judo.modal.confirm.confirm-title', { defaultValue: 'Confirm action' }), + ); + if (confirmed) { + storeDiff('astronomer', null); + } + } catch (error) { + handleError(error, undefined, target); + } + }; + // OpenSelectorAction: God/(esm/_conRARMcEe2_DOUDKkB20Q)/TabularReferenceFieldLinkSetSelectorOpenPageAction/(discriminator/God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessViewPageDefinition) + const astronomerOpenSetSelectorAction = async (): Promise => { + const { result, data: returnedData } = await openViewGalaxyViewGroupDiscovererAstronomerLinkSetSelectorPage({ + ownerData: data, + alreadySelected: data.astronomer ? [data.astronomer] : [], + }); + if (result === 'submit') { + if (Array.isArray(returnedData) && returnedData.length) { + try { + storeDiff('astronomer', returnedData[0]); + return returnedData[0]; + } catch (error) { + console.error(error); + return undefined; + } + } + } + return undefined; + }; + // UnsetAction: God/(esm/_conRARMcEe2_DOUDKkB20Q)/TabularReferenceFieldLinkUnsetAction/(discriminator/God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessViewPageDefinition) + const astronomerUnsetAction = async (target: ViewAstronomer | ViewAstronomerStored) => { + storeDiff('astronomer', null); + }; + // OpenPageAction: God/(esm/_conRARMcEe2_DOUDKkB20Q)/TabularReferenceFieldLinkViewAction/(discriminator/God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessViewPageDefinition) + const astronomerOpenPageAction = async (target: ViewAstronomerStored, isDraftParam?: boolean) => { + // if the `target` is missing we are likely navigating to a relation table page, in which case we need the owner's id + navigate( + routeToViewGalaxyAstronomerRelationViewPage(((target as ViewAstronomerStored) || data).__signedIdentifier), + ); + }; + // BulkDeleteAction: God/(esm/_8AxbAE7tEeycO-gUAWxcVg)/TabularReferenceTableBulkDeleteAction/(discriminator/God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessViewPageDefinition) + const starsBulkDeleteAction = async ( + selectedRows: ViewStarStored[], + ): Promise>> => { + return new Promise((resolve) => { + const selectedIds = selectedRows.map((r) => r.__identifier); + const newList = (data?.stars ?? []).filter((c: any) => !selectedIds.includes(c.__identifier)); + storeDiff('stars', newList); + resolve({ + result: 'delete', + data: [], + }); + }); + }; + // OpenCreateFormAction: God/(esm/_8AxbAE7tEeycO-gUAWxcVg)/TabularReferenceTableCreateAction/(discriminator/God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessViewPageDefinition) + const starsOpenCreateFormAction = async () => { + const itemIndex = (data.stars || []).length; // length gives next without -1-ing it + const { + result, + data: returnedData, + openCreated, + } = await openViewGalaxyStarsRelationFormPage({ + ownerData: produceDataAdjustedOwner(), + ownerValidation: validate, + isDraft: true, + dataPath: `${dataPath ? dataPath + '.' : ''}stars[${itemIndex}]`, + }); + if (result === 'submit' && !editMode) { + await actions.refreshAction!(processQueryCustomizer(getPageQueryCustomizer())); + } else if (result === 'submit-draft' && returnedData) { + const decoratedData = { + ...returnedData, + __identifier: `${draftIdentifierPrefix}${uuidv4()}`, + }; + const newData = [...(data.stars || []), decoratedData]; + storeDiff('stars', newData); + return; + } + if (openCreated && returnedData) { + await starsOpenPageAction(returnedData!); + } + }; + // ExportAction: God/(esm/_8AxbAE7tEeycO-gUAWxcVg)/TabularReferenceTableExportAction/(discriminator/God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessViewPageDefinition) + const starsExportAction = async (queryCustomizer: ViewStarQueryCustomizer) => { + try { + setIsLoading(true); + const response = await godServiceForGalaxiesImpl.exportStars( + { __signedIdentifier: signedIdentifier } as any, + queryCustomizer, + ); + + exportFile(response); + } catch (error) { + console.error(error); + } finally { + setIsLoading(false); + } + }; + // FilterAction: God/(esm/_8AxbAE7tEeycO-gUAWxcVg)/TabularReferenceTableFilterAction/(discriminator/God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessViewPageDefinition) + const starsFilterAction = async ( + id: string, + filterOptions: FilterOption[], + model?: GridFilterModel, + filters?: Filter[], + ): Promise<{ model?: GridFilterModel; filters?: Filter[] }> => { + const newFilters = await openFilterDialog(id, filterOptions, filters); + return { + filters: newFilters, + }; + }; + // RowDeleteAction: God/(esm/_8AxbAE7tEeycO-gUAWxcVg)/TabularReferenceTableRowDeleteAction/(discriminator/God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessViewPageDefinition) + const starsRowDeleteAction = async (target: ViewStarStored) => { + try { + const confirmed = await openConfirmDialog( + 'row-delete-action', + t('judo.modal.confirm.confirm-delete', { + defaultValue: 'Are you sure you would like to delete the selected element?', + }), + t('judo.modal.confirm.confirm-title', { defaultValue: 'Confirm action' }), + ); + if (confirmed) { + const newList = (data?.stars ?? []).filter((c: any) => c.__identifier !== target.__identifier); + storeDiff('stars', newList); + } + } catch (error) { + handleError(error, undefined, target); + } + }; + // OpenPageAction: God/(esm/_8AxbAE7tEeycO-gUAWxcVg)/TabularReferenceTableRowViewAction/(discriminator/God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessViewPageDefinition) + const starsOpenPageAction = async (target: ViewStarStored, isDraftParam?: boolean) => { + const itemIndex = (data.stars || []).findIndex((r) => r.__identifier === target.__identifier)!; + if (isDraftParam) { + const { result, data: returnedData } = await openViewGalaxyStarsRelationViewPage({ + ownerData: produceDataAdjustedOwner(), + isDraft: true, + ownerValidation: validate, + dataPath: `${dataPath ? dataPath + '.' : ''}stars[${itemIndex!}]`, + }); + // we might need to differentiate result handling between operation inputs and crud relation creates + if (result === 'submit-draft' && returnedData) { + const existingIndex = (data.stars || []).findIndex( + (r: { __identifier?: string }) => r.__identifier === returnedData.__identifier, + ); + if (existingIndex > -1) { + // we need to create a copy to keep order, and because mui datagrid freezes elements, and crashes on reference updates + const updatedList = [...(data.stars || [])]; + updatedList[existingIndex] = { + ...returnedData, + }; + storeDiff('stars', updatedList); + } + return; + } + if (result === 'delete' && returnedData) { + const existingIndex = (data.stars || []).findIndex( + (r: { __identifier?: string }) => r.__identifier === returnedData.__identifier, + ); + if (existingIndex > -1) { + // we need to create a copy to keep order, and because mui datagrid freezes elements, and crashes on reference updates + const updatedList = [...(data.stars || [])]; + updatedList.splice(existingIndex, 1); + storeDiff('stars', updatedList); + } + return; + } + if (result === 'close') { + return; + } + } else { + await openViewGalaxyStarsRelationViewPage({ + ownerData: target!, + }); + await refresh(); + } + }; + // OpenOperationInputFormAction: God/(esm/_5NwrQFyrEeylCdga_wJIBQ)/OperationFormCallAction/(discriminator/God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessViewPageDefinition) + const createDarkMatterAction = async (isDraft?: boolean, ownerValidation?: (target: any) => Promise) => { + const { result, data: returnedData } = await openViewGalaxyViewCreateDarkMatterInputForm({ + ownerData: data, + }); + if (result === 'submit' && !editMode) { + await refresh(); + } + }; + // OpenOperationInputFormAction: God/(esm/_5Nx5YFyrEeylCdga_wJIBQ)/OperationFormCallAction/(discriminator/God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessViewPageDefinition) + const createIntergalacticDustAction = async (isDraft?: boolean, ownerValidation?: (target: any) => Promise) => { + const { result, data: returnedData } = await openViewGalaxyViewCreateIntergalacticDustInputForm({ + ownerData: data, + }); + if (result === 'submit' && !editMode) { + await refresh(); + } + }; + // OpenOperationInputFormAction: God/(esm/_5Nx5YVyrEeylCdga_wJIBQ)/OperationFormCallAction/(discriminator/God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessViewPageDefinition) + const createInterstellarMediumAction = async ( + isDraft?: boolean, + ownerValidation?: (target: any) => Promise, + ) => { + const { result, data: returnedData } = await openViewGalaxyViewCreateInterstellarMediumInputForm({ + ownerData: data, + }); + if (result === 'submit' && !editMode) { + await refresh(); + } + }; + // OpenPageAction: God/(esm/_8A3hoE7tEeycO-gUAWxcVg)/TabularReferenceFieldButtonOpenPageAction/(discriminator/God/(esm/_4pyPkM_cEe6fibzd7gNETg)/AccessViewPageDefinition) + const matterOpenPageAction = async (target: ViewMatterStored, isDraftParam?: boolean) => { + // if the `target` is missing we are likely navigating to a relation table page, in which case we need the owner's id + navigate(routeToViewGalaxyMatterRelationTablePage(((target as ViewMatterStored) || data).__signedIdentifier)); + }; + + const actions: ViewGalaxyViewPageActions = { + getPageTitle, + backAction, + cancelAction, + deleteAction, + refreshAction, + updateAction, + astronomerAutocompleteRangeAction, + astronomerAutocompleteSetAction, + astronomerOpenCreateFormAction, + astronomerRowDeleteAction, + astronomerOpenSetSelectorAction, + astronomerUnsetAction, + astronomerOpenPageAction, + starsBulkDeleteAction, + starsOpenCreateFormAction, + starsExportAction, + starsFilterAction, + starsRowDeleteAction, + starsOpenPageAction, + createDarkMatterAction, + createIntergalacticDustAction, + createInterstellarMediumAction, + matterOpenPageAction, + getMask, + getAstronomerMask, + getStarsMask, + ...(customActions ?? {}), + }; + + // ViewModel setup + const viewModel: ViewGalaxyViewViewModel = { + actions, + isLoading, + setIsLoading, + refreshCounter, + editMode, + setEditMode, + refresh, + data, + validation, + setValidation, + storeDiff, + submit, + isFormUpdateable, + isFormDeleteable, + }; + + // Effect section + useEffect(() => { + (async () => { + await actions.refreshAction!(getPageQueryCustomizer()); + })(); + }, []); + + return ( + + +
+ + + + + + ); +} diff --git a/judo-ui-react-itest/ActionGroupTestPro/action_group_test_pro__god/pom.xml b/judo-ui-react-itest/ActionGroupTestPro/action_group_test_pro__god/pom.xml index 91621de6..5513af9f 100644 --- a/judo-ui-react-itest/ActionGroupTestPro/action_group_test_pro__god/pom.xml +++ b/judo-ui-react-itest/ActionGroupTestPro/action_group_test_pro__god/pom.xml @@ -93,7 +93,7 @@ ${ui-model} ${generation-target} - true + false ${model-name} ${appScope} ${appVersion} diff --git a/judo-ui-react-itest/CRUDActionsTest/crudactions_test__actors__actor/pom.xml b/judo-ui-react-itest/CRUDActionsTest/crudactions_test__actors__actor/pom.xml index d64ca21f..840d1126 100644 --- a/judo-ui-react-itest/CRUDActionsTest/crudactions_test__actors__actor/pom.xml +++ b/judo-ui-react-itest/CRUDActionsTest/crudactions_test__actors__actor/pom.xml @@ -93,7 +93,7 @@ ${ui-model} ${generation-target} - true + false ${model-name} ${appScope} ${appVersion} diff --git a/judo-ui-react-itest/CRUDActionsTest/crudactions_test__actors__collections__collection_dashboard_actor/pom.xml b/judo-ui-react-itest/CRUDActionsTest/crudactions_test__actors__collections__collection_dashboard_actor/pom.xml index ac4a857d..91cd1fcb 100644 --- a/judo-ui-react-itest/CRUDActionsTest/crudactions_test__actors__collections__collection_dashboard_actor/pom.xml +++ b/judo-ui-react-itest/CRUDActionsTest/crudactions_test__actors__collections__collection_dashboard_actor/pom.xml @@ -93,7 +93,7 @@ ${ui-model} ${generation-target} - true + false ${model-name} ${appScope} ${appVersion} diff --git a/judo-ui-react-itest/CRUDActionsTest/crudactions_test__actors__singles__single_dashboard_actor/pom.xml b/judo-ui-react-itest/CRUDActionsTest/crudactions_test__actors__singles__single_dashboard_actor/pom.xml index 80993b57..8a47a2e5 100644 --- a/judo-ui-react-itest/CRUDActionsTest/crudactions_test__actors__singles__single_dashboard_actor/pom.xml +++ b/judo-ui-react-itest/CRUDActionsTest/crudactions_test__actors__singles__single_dashboard_actor/pom.xml @@ -93,7 +93,7 @@ ${ui-model} ${generation-target} - true + false ${model-name} ${appScope} ${appVersion} diff --git a/judo-ui-react-itest/OperationParametersTest/operation_parameters_test__actor/pom.xml b/judo-ui-react-itest/OperationParametersTest/operation_parameters_test__actor/pom.xml index a25f110d..312c8db5 100644 --- a/judo-ui-react-itest/OperationParametersTest/operation_parameters_test__actor/pom.xml +++ b/judo-ui-react-itest/OperationParametersTest/operation_parameters_test__actor/pom.xml @@ -93,7 +93,7 @@ ${ui-model} ${generation-target} - true + false ${model-name} ${appScope} ${appVersion} diff --git a/judo-ui-react-itest/RelationTest/relation_test__actor/pom.xml b/judo-ui-react-itest/RelationTest/relation_test__actor/pom.xml index 7d4d6c28..70d1ae3d 100644 --- a/judo-ui-react-itest/RelationTest/relation_test__actor/pom.xml +++ b/judo-ui-react-itest/RelationTest/relation_test__actor/pom.xml @@ -93,7 +93,7 @@ ${ui-model} ${generation-target} - true + false ${model-name} ${appScope} ${appVersion} diff --git a/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiPageContainerHelper.java b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiPageContainerHelper.java index e90d75d0..d9e6971d 100644 --- a/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiPageContainerHelper.java +++ b/judo-ui-react/src/main/java/hu/blackbelt/judo/ui/generator/react/UiPageContainerHelper.java @@ -504,4 +504,13 @@ public static PageDefinition getCallerPageForOperationFormCallButton(Button butt .filter(p -> p.getActions().stream().anyMatch(a -> a.getActionDefinition().equals(button.getActionDefinition()))) .findFirst().orElse(null); } + + public static boolean isVisualElementContainerButton(VisualElement visualElement) { + if (visualElement instanceof Button button) { + if (button.eContainer() instanceof ButtonGroup buttonGroup) { + return buttonGroup.eContainer() instanceof PageContainer; + } + } + return false; + } } 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 352aa9d0..8577da23 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 @@ -58,6 +58,18 @@ public static SortedSet getVisualElementsWithCustomImplementation return result; } + public static SortedSet getContainerActionsVisualElementsWithCustomImplementation(PageContainer container) { + SortedSet result = new TreeSet<>(Comparator.comparing((VisualElement v) -> v.getFQName().trim())); + + collectVisualElementsMatchingCondition(container.getActionButtonGroup(), VisualElement::isCustomImplementation, result); + + return result; + } + + public static boolean containerHasActionsWithCustomImplementation(PageContainer container) { + return !getContainerActionsVisualElementsWithCustomImplementation(container).isEmpty(); + } + public static String pageActionFQName(Action action) { String adn = simpleActionDefinitionName(action.getActionDefinition()); PageDefinition pageDefinition = (PageDefinition) action.eContainer(); diff --git a/judo-ui-react/src/main/resources/actor/src/containers/customization.ts.hbs b/judo-ui-react/src/main/resources/actor/src/containers/customization.ts.hbs index 9daa66c4..498ef46f 100644 --- a/judo-ui-react/src/main/resources/actor/src/containers/customization.ts.hbs +++ b/judo-ui-react/src/main/resources/actor/src/containers/customization.ts.hbs @@ -1,6 +1,6 @@ {{# unless (containerIsEmptyDashboard container) }} import type { FC } from 'react'; - import type { CustomFormVisualElementProps } from '~/custom'; + import type { CustomFormVisualElementProps, CustomFormVisualElementPropsWithActions } from '~/custom'; {{# each (getContainerApiImports container) as |imp| }} import type { {{ classDataName imp '' }}, {{ classDataName imp 'Stored' }} } from '~/services/data-api/model/{{ classDataName imp '' }}'; {{/ each }} @@ -13,7 +13,7 @@ {{# unless (containerIsEmptyDashboard container) }} {{# each (getVisualElementsWithCustomImplementation container) as |ve| }} export const {{ getCustomizationComponentInterfaceKey ve }} = '{{ getCustomizationComponentInterface ve }}'; - export interface {{ getCustomizationComponentInterface ve }} extends FC> {} + export interface {{ getCustomizationComponentInterface ve }} extends FC> {} {{/ each }} export const {{ camelCaseNameToInterfaceKey (containerComponentName container) }}_CONTAINER_ACTIONS_HOOK_INTERFACE_KEY = '{{ camelCaseNameToInterfaceKey (containerComponentName container) }}_CONTAINER_ACTIONS_HOOK'; 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 5e803a5f..2912ab62 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 @@ -2,6 +2,11 @@ import { lazy, Suspense{{# if (containerHasCreateAction container) }}, useRef, useState, useCallback{{/ if }} } from 'react'; import type { Dispatch, SetStateAction } from 'react'; +{{# if (containerHasActionsWithCustomImplementation container) }} + import { OBJECTCLASS } from '@pandino/pandino-api'; + import { ComponentProxy, useTrackService } from '@pandino/react-hooks'; + import { CUSTOM_VISUAL_ELEMENT_INTERFACE_KEY } from '~/custom'; +{{/ if }} import Grid from '@mui/material/Grid'; import Button from '@mui/material/Button'; import DialogTitle from '@mui/material/DialogTitle'; @@ -39,6 +44,14 @@ import { useConfirmDialog } from '~/components/dialog'; {{ containerComponentName container }}DialogActions, {{ containerComponentName container }}DialogProps, } from './types'; + + {{# if (containerHasActionsWithCustomImplementation container) }} + import { + {{# each (getContainerActionsVisualElementsWithCustomImplementation container) as |ve| }} + {{ getCustomizationComponentInterfaceKey ve }}, + {{/ each }} + } from './customization'; + {{/ if }} {{/ unless }} {{# unless (containerIsEmptyDashboard container) }} @@ -160,6 +173,23 @@ export default function {{ containerComponentName container }}Dialog({{# unless {{/ unless }} {{/ if }} + {{# if button.customImplementation }} + + {{/ if }} + {{# if button.customImplementation }} + + {{/ if }} {{# if button.actionDefinition.isCreateAction }} {{# unless button.actionDefinition.autoOpenAfterCreate }} {!isDraft &&