diff --git a/.flowconfig b/.flowconfig new file mode 100644 index 00000000..6b1ca7d9 --- /dev/null +++ b/.flowconfig @@ -0,0 +1,27 @@ +[options] +types_first=false +esproposal.decorators=ignore +esproposal.optional_chaining=enable +module.name_mapper='.+\.s?css' -> 'empty/object' +module.name_mapper='fos-jsrouting/router' -> 'empty/object' + +[untyped] +.*/node_modules/react-leaflet/.* + +[ignore] +.*/node_modules/bower-json/.* +.*/node_modules/config-chain/.* +.*/node_modules/findup/.* +.*/node_modules/jss/.* +.*/node_modules/npmconf/.* +.*/node_modules/stylelint/.* +.*/node_modules/resolve/test/resolver/malformed_package_json/.* +.*/vendor/symfony/.* + +.git/ +.github/ +bin/ +config/ +public/ +templates/ +var/ diff --git a/.gitignore b/.gitignore index b75e6d14..bd2da3cb 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,8 @@ bower_components/ .sass-cache/ node_modules/ npm-debug.log +package-lock.json +yarn.lock # tests /Tests/Application/*.local diff --git a/Admin/FormAdmin.php b/Admin/FormAdmin.php index e94dc251..891add03 100644 --- a/Admin/FormAdmin.php +++ b/Admin/FormAdmin.php @@ -15,6 +15,7 @@ use Sulu\Bundle\AdminBundle\Admin\Navigation\NavigationItem; use Sulu\Bundle\AdminBundle\Admin\Navigation\NavigationItemCollection; use Sulu\Bundle\AdminBundle\Admin\View\DropdownToolbarAction; +use Sulu\Bundle\AdminBundle\Admin\View\ListItemAction; use Sulu\Bundle\AdminBundle\Admin\View\ToolbarAction; use Sulu\Bundle\AdminBundle\Admin\View\ViewBuilderFactoryInterface; use Sulu\Bundle\AdminBundle\Admin\View\ViewCollection; @@ -78,7 +79,7 @@ public function configureViews(ViewCollection $viewCollection): void { $formLocales = \array_values( \array_map( - function(Localization $localization) { + static function(Localization $localization) { return $localization->getLocale(); }, $this->webspaceManager->getAllLocalizations() @@ -155,8 +156,20 @@ function(Localization $localization) { ->addRouterAttributesToListRequest(['id' => 'form']) ->addRouterAttributesToListMetadata(['id' => 'id']) ->addToolbarActions($dataListToolbarActions) + ->addItemActions([ + new ListItemAction('sulu_form.dynamic_preview_item_action'), + ]) ->setParent(static::EDIT_FORM_VIEW) ); + $viewCollection->add( + $this->viewBuilderFactory->createViewBuilder( + 'sulu_form.edit_form.data_details', + '/forms/:locale/:formId/data/details/:id', + 'sulu_form.dynamic_preview' + ) + ->setOption('dataListView', static::LIST_VIEW_DATA) + ->setOption('dataResourceKey', 'dynamic_forms') + ); } public function getSecurityContexts() diff --git a/Controller/DynamicController.php b/Controller/DynamicController.php index 010df145..cdea542c 100644 --- a/Controller/DynamicController.php +++ b/Controller/DynamicController.php @@ -26,6 +26,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Controller to create dynamic form entries list. @@ -124,13 +125,47 @@ public function cgetAction(Request $request): Response return $this->viewHandler->handle($this->view($representation)); } + public function getAction(Request $request, int $id): Response + { + /** @var null|Dynamic $dynamic */ + $dynamic = $this->dynamicRepository->find($id); + + if (!$dynamic) { + throw new NotFoundHttpException(\sprintf('No dynamic with id "%s" was found!', $id)); + } + + return $this->viewHandler->handle($this->view($this->getApiEntity($dynamic))); + } + + /** + * @return array + */ + private function getApiEntity(Dynamic $dynamic): array + { + return [ + 'id' => $dynamic->getId(), + 'type' => $dynamic->getType(), + 'typeId' => $dynamic->getTypeId(), + 'locale' => $dynamic->getLocale(), + 'webspaceKey' => $dynamic->getWebspaceKey(), + 'typeName' => $dynamic->getTypeName(), + 'created' => $dynamic->getCreated(), + 'changed' => $dynamic->getChanged(), + 'data' => $dynamic->getData(), + ]; + } + /** * Delete dynamic form entry. */ - public function deleteAction(Request $request, int $id): Response + public function deleteAction(int $id): Response { $dynamic = $this->dynamicRepository->find($id); + if (!$dynamic) { + throw new NotFoundHttpException(\sprintf('No dynamic with id "%s" was found!', $id)); + } + $attachments = \array_filter(\array_values($dynamic->getFieldsByType(Dynamic::TYPE_ATTACHMENT))); foreach ($attachments as $mediaIds) { diff --git a/Resources/js/index.js b/Resources/js/index.js new file mode 100644 index 00000000..67a2701d --- /dev/null +++ b/Resources/js/index.js @@ -0,0 +1,8 @@ +// @flow +import {listItemActionRegistry} from 'sulu-admin-bundle/views'; +import {viewRegistry} from 'sulu-admin-bundle/containers'; +import DynamicPreview from './views/DynamicPreview'; +import DynamicPreviewItemAction from './listItemActions/DynamicPreviewItemAction'; + +listItemActionRegistry.add('sulu_form.dynamic_preview_item_action', DynamicPreviewItemAction); +viewRegistry.add('sulu_form.dynamic_preview', DynamicPreview); diff --git a/Resources/js/listItemActions/DynamicPreviewItemAction.js b/Resources/js/listItemActions/DynamicPreviewItemAction.js new file mode 100644 index 00000000..a7f46996 --- /dev/null +++ b/Resources/js/listItemActions/DynamicPreviewItemAction.js @@ -0,0 +1,22 @@ +// @flow +import {AbstractListItemAction} from 'sulu-admin-bundle/views'; + +export default class DynamicPreviewItemAction extends AbstractListItemAction { + getItemActionConfig(item: ?Object) { + return { + icon: 'su-eye', + disabled: false, + onClick: item ? () => this.handleClick(item) : undefined, + }; + } + + handleClick = (item) => { + this.router.navigate( + 'sulu_form.edit_form.data_details', + { + formId: this.listStore.metadataOptions.id, + id: item.id + } + ) + }; +} diff --git a/Resources/js/package.json b/Resources/js/package.json new file mode 100644 index 00000000..5ef73ab6 --- /dev/null +++ b/Resources/js/package.json @@ -0,0 +1,15 @@ +{ + "name": "sulu-form-bundle", + "description": "Adds support for forms to Sulu", + "license": "MIT", + "repository": "https://github.com/sulu/sulu.git", + "devDependencies": { + "sulu-admin-bundle": "file:../../vendor/sulu/sulu/src/Sulu/Bundle/AdminBundle/Resources/js" + }, + "peerDependencies": { + "mobx": "^4.0.0", + "mobx-react": "^5.0.0", + "react": "^16.2.0 || ^17.0.0", + "sulu-admin-bundle": "*" + } +} diff --git a/Resources/js/views/DynamicPreview.js b/Resources/js/views/DynamicPreview.js new file mode 100644 index 00000000..c8d50ac7 --- /dev/null +++ b/Resources/js/views/DynamicPreview.js @@ -0,0 +1,92 @@ +// @flow +import React from 'react'; +import {observer} from 'mobx-react'; +import {action, observable} from 'mobx'; +import {Loader} from 'sulu-admin-bundle/components'; +import Snackbar from 'sulu-admin-bundle/components/Snackbar'; +import {withToolbar} from 'sulu-admin-bundle/containers'; +import {ResourceRequester} from 'sulu-admin-bundle/services'; + +@observer +class DynamicPreview extends React.Component { + @observable loading = false; + @observable data = undefined; + + componentDidMount() { + this.loadData(); + } + + @action loadData = () => { + this.loading = true; + this.data = undefined; + + const dataResourceKey = this.props.router.route.options.dataResourceKey; + + return ResourceRequester.get(dataResourceKey, {id: this.getDataId()}) + .then(action(response => { + console.log('RESPONSE', response); + this.data = response; + })) + .catch(e => { + console.error('Error while loading dynamic form data from server.', e); + }) + .finally(action(() => { + this.loading = false; + })); + } + + @action navigateToFormData = () => { + const routeAttributes = this.props.router.attributes; + this.props.router.navigate( + this.props.router.route.options.dataListView, + { + locale: routeAttributes.locale, + id: routeAttributes.formId, + } + ) + } + + getDataId() { + return this.props.router.attributes.id; + } + + render() { + if (this.loading) { + return ; + } + + if (!this.data) { + return + } + + return ( +
+

Form submission from {this.data.typeName}

+ Submitted at: {this.data.created} +
+ {Object.keys(this.data.data).map((key, index) => ( + +
{key}
+
{this.data.data[key]}
+
+ ))} +
+
+ ); + } +} + +export default withToolbar(DynamicPreview, function () { + return { + items: [ + { + type: 'button', + icon: 'su-angle-left', + disabled: false, + onClick: () => { + this.navigateToFormData(); + }, + } + ] + }; +}); diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index eafa423c..5a0b31c9 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -170,11 +170,6 @@ parameters: count: 1 path: Controller/DynamicController.php - - - message: "#^Cannot call method getFieldsByType\\(\\) on Sulu\\\\Bundle\\\\FormBundle\\\\Entity\\\\Dynamic\\|null\\.$#" - count: 1 - path: Controller/DynamicController.php - - message: "#^Cannot cast mixed to int\\.$#" count: 1 @@ -195,11 +190,6 @@ parameters: count: 1 path: Controller/DynamicController.php - - - message: "#^Parameter \\#1 \\$entity of method Doctrine\\\\ORM\\\\EntityManager\\:\\:remove\\(\\) expects object, Sulu\\\\Bundle\\\\FormBundle\\\\Entity\\\\Dynamic\\|null given\\.$#" - count: 1 - path: Controller/DynamicController.php - - message: "#^Parameter \\#1 \\$id of method Sulu\\\\Bundle\\\\MediaBundle\\\\Media\\\\Manager\\\\MediaManagerInterface\\:\\:delete\\(\\) expects int, mixed given\\.$#" count: 1 @@ -1704,4 +1694,3 @@ parameters: message: "#^Call to an undefined method Symfony\\\\Component\\\\HttpFoundation\\\\RequestStack\\:\\:getMasterRequest\\(\\)\\.$#" count: 1 path: TitleProvider/StructureTitleProvider.php -