diff --git a/bundle/Controller/Admin/AdminController.php b/bundle/Controller/Admin/AdminController.php index b211c2f2..a377fb8e 100644 --- a/bundle/Controller/Admin/AdminController.php +++ b/bundle/Controller/Admin/AdminController.php @@ -1,24 +1,34 @@ service = $service; $this->contentService = $contentService; $this->configResolver = $configResolver; $this->anonymizer = $anonymizer; $this->translator = $translator; + $this->factory = $factory; + $this->infoCollectionRepository = $infoCollectionRepository; + $this->infoCollectionAttributeRepository = $infoCollectionAttributeRepository; + $this->builder = $builder; } /** - * Displays overview page + * Displays overview page. */ public function overviewAction(Request $request): Response { @@ -63,27 +91,27 @@ public function overviewAction(Request $request): Response $adapter = new InformationCollectionContentsAdapter($this->service, Query::countQuery()); $pager = $this->getPager($adapter, (int) $request->query->get('page')); - return $this->render("@NetgenInformationCollection/admin/overview.html.twig", ['objects' => $pager]); + return $this->render('@NetgenInformationCollection/admin/overview.html.twig', ['objects' => $pager]); } /** - * Displays list of collection for selected Content + * Displays list of collection for selected Content. */ public function collectionListAction(Request $request, Content $content): Response { $this->checkReadPermissions(); $adapter = new InformationCollectionCollectionListAdapter($this->service, ContentId::withContentId($content->id)); - $pager = $this->getPager($adapter, (int)$request->query->get('page')); + $pager = $this->getPager($adapter, (int) $request->query->get('page')); - return $this->render("@NetgenInformationCollection/admin/collection_list.html.twig", [ + return $this->render('@NetgenInformationCollection/admin/collection_list.html.twig', [ 'objects' => $pager, 'content' => $content, ]); } /** - * Handles collection search + * Handles collection search. */ public function searchAction(Request $request, Content $content): Response { @@ -92,9 +120,10 @@ public function searchAction(Request $request, Content $content): Response $query = SearchQuery::withContentAndSearchText($content->id, $request->query->get('searchText')); $adapter = new InformationCollectionCollectionListSearchAdapter($this->service, $query); - $pager = $this->getPager($adapter, (int)$request->query->get('page')); + $pager = $this->getPager($adapter, (int) $request->query->get('page')); - return $this->render("@NetgenInformationCollection/admin/collection_list.html.twig", + return $this->render( + '@NetgenInformationCollection/admin/collection_list.html.twig', [ 'objects' => $pager, 'content' => $content, @@ -103,20 +132,20 @@ public function searchAction(Request $request, Content $content): Response } /** - * Displays individual collection details + * Displays individual collection details. */ public function viewAction(Collection $collection): Response { $this->checkReadPermissions(); - return $this->render("@NetgenInformationCollection/admin/view.html.twig", [ + return $this->render('@NetgenInformationCollection/admin/view.html.twig', [ 'collection' => $collection, 'content' => $collection->getContent(), ]); } /** - * Handles actions performed on overview page + * Handles actions performed on overview page. */ public function handleContentsAction(Request $request): RedirectResponse { @@ -132,7 +161,6 @@ public function handleContentsAction(Request $request): RedirectResponse } if ($request->request->has('DeleteCollectionByContentAction')) { - $this->checkDeletePermissions(); $query = new Contents($contents); @@ -150,7 +178,7 @@ public function handleContentsAction(Request $request): RedirectResponse } /** - * Handles actions performed on collection list page + * Handles actions performed on collection list page. */ public function handleCollectionListAction(Request $request): RedirectResponse { @@ -167,10 +195,9 @@ public function handleCollectionListAction(Request $request): RedirectResponse } if ($request->request->has('DeleteCollectionAction')) { - $this->checkDeletePermissions(); - $query = new Collections($contentId, $collections); + $query = new Collections((int)$contentId, $collections); $this->service->deleteCollections($query); @@ -180,7 +207,6 @@ public function handleCollectionListAction(Request $request): RedirectResponse } if ($request->request->has('AnonymizeCollectionAction')) { - $this->checkAnonymizePermissions(); foreach ($collections as $collection) { @@ -198,7 +224,7 @@ public function handleCollectionListAction(Request $request): RedirectResponse } /** - * Handles action on collection details page + * Handles action on collection details page. */ public function handleCollectionAction(Request $request): RedirectResponse { @@ -219,7 +245,6 @@ public function handleCollectionAction(Request $request): RedirectResponse } if ($request->request->has('DeleteFieldAction')) { - $this->checkDeletePermissions(); $query = new CollectionFields($contentId, $collectionId, $fields); @@ -232,7 +257,6 @@ public function handleCollectionAction(Request $request): RedirectResponse } if ($request->request->has('AnonymizeFieldAction')) { - $this->checkAnonymizePermissions(); $this->anonymizer->anonymizeCollection($collectionId, $fields); @@ -243,25 +267,22 @@ public function handleCollectionAction(Request $request): RedirectResponse } if ($request->request->has('DeleteCollectionAction')) { - $this->checkDeletePermissions(); - $query = new Collections($contentId, [$collectionId]); + $query = new Collections((int)$contentId, [$collectionId]); $this->service->deleteCollections($query); - $this->addFlashMessage("success", "collection_removed"); + $this->addFlashMessage('success', 'collection_removed'); return $this->redirectToRoute('netgen_information_collection.route.admin.collection_list', ['contentId' => $contentId]); - } if ($request->request->has('AnonymizeCollectionAction')) { - $this->checkAnonymizePermissions(); $this->anonymizer->anonymizeCollection($collectionId); - $this->addFlashMessage("success", "collection_anonymized"); + $this->addFlashMessage('success', 'collection_anonymized'); return $this->redirectToRoute('netgen_information_collection.route.admin.view', ['collectionId' => $collectionId]); } @@ -271,10 +292,95 @@ public function handleCollectionAction(Request $request): RedirectResponse return $this->redirectToRoute('netgen_information_collection.route.admin.view', ['collectionId' => $collectionId]); } + /** + * @throws NonUniqueResultException + */ + public function editAction(Request $request, int $collectionId): RedirectResponse|Response + { + $this->checkEditPermissions(); + + $collection = $this->service->getCollection(new CollectionId($collectionId)); + + $location = $collection->getContent()->contentInfo->getMainLocation(); + + if ($location === null) { + throw $this->createNotFoundException(); + } + + $form = $this->builder->createUpdateFormForLocation($location, $collection)->getForm(); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + /** @var InformationCollectionStruct $struct */ + $struct = $form->getData()->payload; + + /** @var ContentType $contentType */ + $contentType = $form->getData()->definition; + + $ezInfoCollection = $this->infoCollectionRepository->find($collectionId); + + if ($ezInfoCollection === null) { + throw $this->createNotFoundException(); + } + + $ezInfoCollection->setModified(time()); + + $this->infoCollectionRepository->save($ezInfoCollection); + + foreach ($struct->getCollectedFields() as $fieldDefIdentifier => $value) { + if ($value === null) { + continue; + } + + $fieldDefinition = $contentType->getFieldDefinition($fieldDefIdentifier); + + if ($fieldDefinition === null) { + continue; + } + + $legacyValue = $this->factory->getLegacyValue($value, $fieldDefinition); + + $ezInfoAttribute = $this->infoCollectionAttributeRepository->findByCollectionIdAndFieldDefinitionId( + $collectionId, + $fieldDefinition->id + ); + + if ($ezInfoAttribute === null) { + $ezInfoAttribute = $this->infoCollectionAttributeRepository->getInstance(); + $ezInfoAttribute->setContentObjectId($collection->getContent()->id); + $ezInfoAttribute->setContentObjectAttributeId($collection->getContent()->getField($fieldDefinition->identifier)->id); + $ezInfoAttribute->setContentClassAttributeId($fieldDefinition->id); + $ezInfoAttribute->setInformationCollectionId($collection->getId()); + } + + $ezInfoAttribute->setDataInt($legacyValue->getDataInt()); + $ezInfoAttribute->setDataFloat($legacyValue->getDataFloat()); + $ezInfoAttribute->setDataText($legacyValue->getDataText()); + + $this->infoCollectionAttributeRepository->save($ezInfoAttribute); + } + + return $this->redirectToRoute( + 'netgen_information_collection.route.admin.view', + [ + 'contentId' => $location->contentInfo->id, + 'collectionId' => $collection->getId(), + ] + ); + } + + return $this->render('@NetgenInformationCollection/admin/edit.html.twig', [ + 'collection' => $collection, + 'content' => $location->getContent(), + 'form' => $form->createView(), + ]); + } + /** * Adds a flash message with specified parameters. */ - protected function addFlashMessage(string $messageType, string $message, int $count = 1, array $parameters = array()): void + protected function addFlashMessage(string $messageType, string $message, int $count = 1, array $parameters = []): void { $parameters = array_merge($parameters, ['count' => $count]); @@ -289,7 +395,7 @@ protected function addFlashMessage(string $messageType, string $message, int $co } /** - * Returns configured instance of Pagerfanta + * Returns configured instance of Pagerfanta. */ protected function getPager(AdapterInterface $adapter, int $currentPage): Pagerfanta { @@ -320,4 +426,10 @@ protected function checkAnonymizePermissions(): void $attribute = new Attribute('infocollector', 'anonymize'); $this->denyAccessUnlessGranted($attribute); } + + protected function checkEditPermissions(): void + { + $attribute = new Attribute('infocollector', 'edit'); + $this->denyAccessUnlessGranted($attribute); + } } diff --git a/bundle/Controller/Admin/Export/Export.php b/bundle/Controller/Admin/Export/Export.php index acfc0cad..be878557 100644 --- a/bundle/Controller/Admin/Export/Export.php +++ b/bundle/Controller/Admin/Export/Export.php @@ -6,7 +6,7 @@ use Ibexa\Contracts\Core\Repository\ContentService; use Ibexa\Core\MVC\Symfony\Security\Authorization\Attribute; -use Netgen\Bundle\InformationCollectionBundle\Form\ExportType; +use Netgen\Bundle\InformationCollectionBundle\Form\Type\ExportType; use Netgen\InformationCollection\API\Service\Exporter; use Netgen\InformationCollection\API\Value\Export\ExportCriteria; use Netgen\InformationCollection\Core\Export\ExportResponseFormatterRegistry; diff --git a/bundle/DependencyInjection/Compiler/FieldTypeHandlerRegistryPass.php b/bundle/DependencyInjection/Compiler/FieldTypeHandlerRegistryPass.php new file mode 100644 index 00000000..23c4ce7e --- /dev/null +++ b/bundle/DependencyInjection/Compiler/FieldTypeHandlerRegistryPass.php @@ -0,0 +1,41 @@ +hasDefinition('netgen_information_collection.form.fieldtype_handler_registry')) { + return; + } + + $registry = $container->getDefinition('netgen_information_collection.form.fieldtype_handler_registry'); + + foreach ($container->findTaggedServiceIds('netgen_information_collection.form.fieldtype_handler') as $id => $attributes) { + foreach ($attributes as $attribute) { + if (!isset($attribute['alias'])) { + throw new LogicException( + "'netgen_information_collection.form.fieldtype_handler' service tag " . + "needs an 'alias' attribute to identify the field type. None given." + ); + } + + $registry->addMethodCall( + 'register', + [ + $attribute['alias'], + new Reference($id), + ] + ); + } + } + } +} diff --git a/bundle/Form/Builder/FormBuilder.php b/bundle/Form/Builder/FormBuilder.php new file mode 100644 index 00000000..4827e7a9 --- /dev/null +++ b/bundle/Form/Builder/FormBuilder.php @@ -0,0 +1,82 @@ +contentInfo; + $contentType = $this->contentTypeService->loadContentType($contentInfo->contentTypeId); + $struct = new InformationCollectionStruct(); + + foreach ($collection->getAttributes() as $attribute) { + $fieldValue = $this->getFieldValueFromAttribute($attribute); + + if ($fieldValue !== null) { + $struct->setCollectedFieldValue($attribute->getField()->getFieldDefinitionIdentifier(), $fieldValue); + } + } + + $data = new DataWrapper($struct, $contentType, $location); + + $useCsrf = $this->configResolver->getParameter('information_collection.form.use_csrf', 'netgen'); + + return $this->formFactory + ->createNamedBuilder( + sprintf('%s_%d', $contentType->identifier, $location->id), + InformationCollectionUpdateType::class, + $data, + [ + 'collection' => $collection, + 'csrf_protection' => $useCsrf, + ] + ); + } + + private function getFieldValueFromAttribute(Attribute $attribute): ?ValueInterface + { + $handler = $this->registry->handle($attribute->getFieldDefinition()->defaultValue); + if (!$handler instanceof CustomLegacyFieldHandlerInterface) { + return null; + } + + $legacyData = new FieldValue( + $attribute->getField()->id, + $attribute->getValue()->getDataText(), + $attribute->getValue()->getDataInt(), + $attribute->getValue()->getDataFloat() + ); + + return $handler->fromLegacyValue($legacyData); + } +} diff --git a/bundle/Form/DataMapper.php b/bundle/Form/DataMapper.php new file mode 100644 index 00000000..711b59d9 --- /dev/null +++ b/bundle/Form/DataMapper.php @@ -0,0 +1,134 @@ +fieldTypeHandlerRegistry = $fieldTypeHandlerRegistry; + $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); + } + + public function mapDataToForms(mixed $viewData, \Traversable $forms): void + { + $empty = null === $viewData || [] === $viewData; + + if (!$empty && !is_array($viewData) && !is_object($viewData)) { + throw new UnexpectedTypeException($viewData, 'object, array or empty'); + } + + foreach ($forms as $form) { + $propertyPath = $form->getPropertyPath(); + $config = $form->getConfig(); + + if ($viewData instanceof DataWrapper && null !== $propertyPath && $config->getMapped()) { + /* @var $viewData \Netgen\Bundle\InformationCollectionBundle\Form\DataWrapper */ + $this->mapToForm($form, $viewData, $propertyPath); + } elseif (!$empty && null !== $propertyPath && $config->getMapped()) { + $form->setData($this->propertyAccessor->getValue($viewData, $propertyPath)); + } else { + $form->setData($form->getConfig()->getData()); + } + } + } + + public function mapFormsToData(\Traversable $forms, mixed &$viewData): void + { + if (null === $viewData) { + return; + } + + if (!is_array($viewData) && !is_object($viewData)) { + throw new UnexpectedTypeException($viewData, 'object, array or empty'); + } + + foreach ($forms as $form) { + $propertyPath = $form->getPropertyPath(); + $config = $form->getConfig(); + + // Write-back is disabled if the form is not synchronized (transformation failed), + // if the form was not submitted and if the form is disabled (modification not allowed) + if ( + null === $propertyPath + || !$config->getMapped() + || !$form->isSubmitted() + || !$form->isSynchronized() + || $form->isDisabled() + ) { + continue; + } + + // If $data is out ContentCreateStruct, we need to map it to the corresponding field + // in the struct + if ($viewData instanceof DataWrapper) { + $this->mapFromForm($form, $viewData, $propertyPath); + + continue; + } + + // If the field is of type DateTime and the data is the same skip the update to + // keep the original object hash + if ( + $form->getData() instanceof \DateTimeImmutable + && $form->getData() === $this->propertyAccessor->getValue($viewData, $propertyPath) + ) { + continue; + } + + // If the data is identical to the value in $data, we are + // dealing with a reference + if ( + is_object($viewData) + && $config->getByReference() + && $form->getData() === $this->propertyAccessor->getValue($viewData, $propertyPath) + ) { + continue; + } + + $this->propertyAccessor->setValue($viewData, $propertyPath, $form->getData()); + } + } + + /** + * Maps data from Ibexa Platform structure to the form. + */ + abstract protected function mapToForm( + FormInterface $form, + DataWrapper $data, + PropertyPathInterface $propertyPath + ): void; + + /** + * Maps data from form to the Ibexa Platform structure. + */ + abstract protected function mapFromForm( + FormInterface $form, + DataWrapper $data, + PropertyPathInterface $propertyPath + ): void; +} diff --git a/bundle/Form/DataMapper/InformationCollectionUpdateMapper.php b/bundle/Form/DataMapper/InformationCollectionUpdateMapper.php new file mode 100644 index 00000000..37b64a86 --- /dev/null +++ b/bundle/Form/DataMapper/InformationCollectionUpdateMapper.php @@ -0,0 +1,77 @@ +definition; + + $fieldDefinitionIdentifier = (string) $propertyPath; + $fieldDefinition = $contentType->getFieldDefinition($fieldDefinitionIdentifier); + + if ($fieldDefinition === null) { + throw new RuntimeException(sprintf('Data definition does not contain expected FieldDefinition %s', $fieldDefinitionIdentifier)); + } + + $fieldTypeIdentifier = $fieldDefinition->fieldTypeIdentifier; + + $handler = $this->fieldTypeHandlerRegistry->get($fieldTypeIdentifier); + + /** @var InformationCollectionStruct $struct */ + $struct = $data->payload; + + $collectedFieldValue = $struct->getCollectedFieldValue($fieldDefinitionIdentifier); + if ($collectedFieldValue === null) { + return; + } + + $form->setData( + $handler->convertFieldValueToForm( + $struct->getCollectedFieldValue($fieldDefinitionIdentifier), + $fieldDefinition + ) + ); + } + + protected function mapFromForm( + FormInterface $form, + DataWrapper $data, + PropertyPathInterface $propertyPath + ): void { + $payload = $data->payload; + $contentType = $data->definition; + + $fieldDefinitionIdentifier = (string) $propertyPath; + $fieldDefinition = $contentType->getFieldDefinition($fieldDefinitionIdentifier); + + if ($fieldDefinition === null) { + throw new RuntimeException(sprintf('Data definition does not contain expected FieldDefinition %s', $fieldDefinitionIdentifier)); + } + + $fieldTypeIdentifier = $fieldDefinition->fieldTypeIdentifier; + $handler = $this->fieldTypeHandlerRegistry->get($fieldTypeIdentifier); + + $payload->setCollectedFieldValue( + $fieldDefinitionIdentifier, + $handler->convertFieldValueFromForm($form->getData()) + ); + } +} diff --git a/bundle/Form/DataWrapper.php b/bundle/Form/DataWrapper.php new file mode 100644 index 00000000..03267981 --- /dev/null +++ b/bundle/Form/DataWrapper.php @@ -0,0 +1,44 @@ +payload = $payload; + $this->definition = $definition; + $this->target = $target; + } +} diff --git a/bundle/Form/FieldTypeHandler.php b/bundle/Form/FieldTypeHandler.php new file mode 100644 index 00000000..4c6f92c9 --- /dev/null +++ b/bundle/Form/FieldTypeHandler.php @@ -0,0 +1,94 @@ +buildFieldForm($formBuilder, $fieldDefinition, $languageCode); + } + + /** + * In most cases this will be the same as {@link self::buildCreateFieldForm()}. + * For this reason default implementation falls back to the internal method + * {@link self::buildFieldForm()}, which should be implemented as needed. + */ + public function buildFieldUpdateForm( + FormBuilderInterface $formBuilder, + FieldDefinition $fieldDefinition, + Content $content, + string $languageCode + ): void { + $this->buildFieldForm($formBuilder, $fieldDefinition, $languageCode, $content); + } + + /** + * In most cases implementations of methods {@link self::buildCreateFieldForm()} + * and {@link self::buildUpdateFieldForm()} will be the same, therefore default + * handler implementation of those falls back to this method. + * + * Implement as needed. + */ + protected function buildFieldForm( + FormBuilderInterface $formBuilder, + FieldDefinition $fieldDefinition, + string $languageCode, + ?Content $content = null + ): void { + throw new RuntimeException('Not implemented.'); + } + + /** + * Returns default field options, created from given $fieldDefinition and $languageCode. + */ + protected function getDefaultFieldOptions( + FieldDefinition $fieldDefinition, + string $languageCode, + ?Content $content = null + ): array { + $options = []; + + $options['label'] = $fieldDefinition->getName($languageCode); + $options['required'] = $fieldDefinition->isRequired; + $options['ibexa_forms']['description'] = $fieldDefinition->getDescription($languageCode); + $options['ibexa_forms']['language_code'] = $languageCode; + $options['ibexa_forms']['fielddefinition'] = $fieldDefinition; + + if ($content !== null) { + $options['ibexa_forms']['content'] = $content; + } + + $options['constraints'] = []; + if ($fieldDefinition->isRequired) { + $options['constraints'][] = new Constraints\NotBlank(); + } + + return $options; + } +} diff --git a/bundle/Form/FieldTypeHandler/BinaryFile.php b/bundle/Form/FieldTypeHandler/BinaryFile.php new file mode 100644 index 00000000..454b63cd --- /dev/null +++ b/bundle/Form/FieldTypeHandler/BinaryFile.php @@ -0,0 +1,62 @@ + $data->getRealPath(), + 'fileName' => $data->getClientOriginalName(), + 'fileSize' => $data->getSize(), + ]; + + return new FileValue($fileData); + } + + protected function buildFieldForm( + FormBuilderInterface $formBuilder, + FieldDefinition $fieldDefinition, + string $languageCode, + ?Content $content = null + ): void { + $options = $this->getDefaultFieldOptions($fieldDefinition, $languageCode, $content); + $maxFileSize = $fieldDefinition->validatorConfiguration['FileSizeValidator']['maxFileSize'] ?? false; + + if ($maxFileSize !== false) { + $options['constraints'][] = new Constraints\File( + [ + 'maxSize' => $maxFileSize * Constraints\FileValidator::MB_BYTES, + ] + ); + } + + // Used with update for displaying current file + $options['block_name'] = 'ibexa_forms_binary_file'; + + $formBuilder->add($fieldDefinition->identifier, FileType::class, $options); + } +} diff --git a/bundle/Form/FieldTypeHandler/Birthday.php b/bundle/Form/FieldTypeHandler/Birthday.php new file mode 100644 index 00000000..0f5c0aaa --- /dev/null +++ b/bundle/Form/FieldTypeHandler/Birthday.php @@ -0,0 +1,51 @@ +date; + } + + public function convertFieldValueFromForm(mixed $data): BirthdayValue + { + if (empty($data)) { + $data = null; + } + + return new BirthdayValue($data); + } + + protected function buildFieldForm( + FormBuilderInterface $formBuilder, + FieldDefinition $fieldDefinition, + string $languageCode, + ?Content $content = null + ): void { + $options = $this->getDefaultFieldOptions($fieldDefinition, $languageCode, $content); + + $options['input'] = 'datetime_immutable'; + $options['widget'] = 'choice'; + + $formBuilder->add( + $fieldDefinition->identifier, + BirthdayType::class, + $options + ); + } +} diff --git a/bundle/Form/FieldTypeHandler/Checkbox.php b/bundle/Form/FieldTypeHandler/Checkbox.php new file mode 100644 index 00000000..5e87388e --- /dev/null +++ b/bundle/Form/FieldTypeHandler/Checkbox.php @@ -0,0 +1,53 @@ +fieldHelper = $fieldHelper; + } + + + /** + * @param \Ibexa\Core\FieldType\Checkbox\Value $value + */ + public function convertFieldValueToForm(Value $value, ?FieldDefinition $fieldDefinition = null): bool + { + return $value->bool; + } + + public function convertFieldValueFromForm(mixed $data): CheckboxValue + { + return new CheckboxValue($data); + } + + protected function buildFieldForm( + FormBuilderInterface $formBuilder, + FieldDefinition $fieldDefinition, + string $languageCode, + ?Content $content = null + ): void { + $options = $this->getDefaultFieldOptions($fieldDefinition, $languageCode, $content); + + if (!$content instanceof Content && $fieldDefinition->defaultValue instanceof CheckboxValue) { + $options['data'] = $fieldDefinition->defaultValue->bool; + } + + $formBuilder->add($fieldDefinition->identifier, CheckboxType::class, $options); + } +} diff --git a/bundle/Form/FieldTypeHandler/Country.php b/bundle/Form/FieldTypeHandler/Country.php new file mode 100644 index 00000000..77fe150f --- /dev/null +++ b/bundle/Form/FieldTypeHandler/Country.php @@ -0,0 +1,98 @@ +countryData = $countryData; + + foreach ($countryData as $countryCode => $country) { + $this->filteredCountryData[$countryCode] = $country['Name']; + } + } + + /** + * @param \Ibexa\Core\FieldType\Country\Value $value + */ + public function convertFieldValueToForm(Value $value, ?FieldDefinition $fieldDefinition = null): mixed + { + $isMultiple = true; + if ($fieldDefinition !== null) { + $fieldSettings = $fieldDefinition->getFieldSettings(); + $isMultiple = $fieldSettings['isMultiple']; + } + + if (!$isMultiple) { + if (empty($value->countries)) { + return ''; + } + + $keys = array_keys($value->countries); + + return reset($keys); + } + + return array_keys($value->countries); + } + + public function convertFieldValueFromForm(mixed $data): CountryValue + { + $country = []; + + // case if multiple is true + if (is_array($data)) { + foreach ($data as $countryCode) { + if (array_key_exists($countryCode, $this->countryData)) { + $country[$countryCode] = $this->countryData[$countryCode]; + } + } + } elseif (array_key_exists($data, $this->countryData)) { + $country[$data] = $this->countryData[$data]; + } + + return new CountryValue($country); + } + + protected function buildFieldForm( + FormBuilderInterface $formBuilder, + FieldDefinition $fieldDefinition, + string $languageCode, + ?Content $content = null + ): void { + $options = $this->getDefaultFieldOptions($fieldDefinition, $languageCode, $content); + + $options['expanded'] = false; + $options['multiple'] = $fieldDefinition->getFieldSettings()['isMultiple'] ?? false; + + $options['choices'] = array_flip($this->filteredCountryData); + + $formBuilder->add($fieldDefinition->identifier, ChoiceType::class, $options); + } +} diff --git a/bundle/Form/FieldTypeHandler/Date.php b/bundle/Form/FieldTypeHandler/Date.php new file mode 100644 index 00000000..b7f5f436 --- /dev/null +++ b/bundle/Form/FieldTypeHandler/Date.php @@ -0,0 +1,46 @@ +date; + } + + public function convertFieldValueFromForm(mixed $data): DateValue + { + return new DateValue($data); + } + + protected function buildFieldForm( + FormBuilderInterface $formBuilder, + FieldDefinition $fieldDefinition, + string $languageCode, + ?Content $content = null + ): void { + $options = $this->getDefaultFieldOptions($fieldDefinition, $languageCode, $content); + + $options['input'] = 'datetime'; + $options['widget'] = 'choice'; + $options['constraints'][] = new Assert\Date(); + + $formBuilder->add($fieldDefinition->identifier, DateType::class, $options); + } +} diff --git a/bundle/Form/FieldTypeHandler/DateAndTime.php b/bundle/Form/FieldTypeHandler/DateAndTime.php new file mode 100644 index 00000000..71d87589 --- /dev/null +++ b/bundle/Form/FieldTypeHandler/DateAndTime.php @@ -0,0 +1,49 @@ +value; + } + + public function convertFieldValueFromForm(mixed $data): DateTimeValue + { + return new DateTimeValue($data); + } + + protected function buildFieldForm( + FormBuilderInterface $formBuilder, + FieldDefinition $fieldDefinition, + string $languageCode, + ?Content $content = null + ): void { + $options = $this->getDefaultFieldOptions($fieldDefinition, $languageCode, $content); + + $useSeconds = $fieldDefinition->getFieldSettings()['useSeconds'] ?? false; + $options['input'] = 'datetime'; + $options['date_widget'] = 'choice'; + $options['time_widget'] = 'choice'; + $options['with_seconds'] = $useSeconds; + $options['constraints'][] = new Assert\DateTime(); + + $formBuilder->add($fieldDefinition->identifier, DateTimeType::class, $options); + } +} diff --git a/bundle/Form/FieldTypeHandler/Email.php b/bundle/Form/FieldTypeHandler/Email.php new file mode 100644 index 00000000..169854fc --- /dev/null +++ b/bundle/Form/FieldTypeHandler/Email.php @@ -0,0 +1,45 @@ +email; + } + + public function convertFieldValueFromForm(mixed $data): EmailAddressValue + { + return new EmailAddressValue($data); + } + + protected function buildFieldForm( + FormBuilderInterface $formBuilder, + FieldDefinition $fieldDefinition, + string $languageCode, + ?Content $content = null + ): void { + $options = $this->getDefaultFieldOptions($fieldDefinition, $languageCode, $content); + + if (isset($fieldDefinition->validatorConfiguration['EmailAddressValidator'])) { + $options['constraints'][] = new Constraints\Email(); + } + + $formBuilder->add($fieldDefinition->identifier, EmailType::class, $options); + } +} diff --git a/bundle/Form/FieldTypeHandler/EnhancedSelection.php b/bundle/Form/FieldTypeHandler/EnhancedSelection.php new file mode 100644 index 00000000..f0f61b28 --- /dev/null +++ b/bundle/Form/FieldTypeHandler/EnhancedSelection.php @@ -0,0 +1,83 @@ +getFieldSettings(); + $isMultiple = $fieldSettings['isMultiple']; + } + + if (!$isMultiple) { + if (empty($value->identifiers)) { + return ''; + } + + return $value->identifiers[0]; + } + + return $value->identifiers; + } + + public function convertFieldValueFromForm(mixed $data): EnhancedSelectionValue + { + if ($data === null) { + return new EnhancedSelectionValue(); + } + + return new EnhancedSelectionValue(is_array($data) ? $data : [$data]); + } + + protected function buildFieldForm( + FormBuilderInterface $formBuilder, + FieldDefinition $fieldDefinition, + string $languageCode, + ?Content $content = null + ): void { + $options = $this->getDefaultFieldOptions($fieldDefinition, $languageCode, $content); + + $fieldSettings = $fieldDefinition->getFieldSettings(); + $optionsValues = $fieldSettings['options']; + + $options['multiple'] = $fieldSettings['isMultiple']; + $options['expanded'] = $fieldSettings['isExpanded']; + $options['choices'] = $this->getValues($optionsValues); + + $formBuilder->add( + $fieldDefinition->identifier, + ChoiceType::class, + $options + ); + } + + private function getValues(array $options): array + { + $values = []; + + foreach ($options as $option) { + if (!empty($option['identifier']) && !empty($option['name'])) { + $values[$option['name']] = $option['identifier']; + } + } + + return $values; + } +} + diff --git a/bundle/Form/FieldTypeHandler/FloatHandler.php b/bundle/Form/FieldTypeHandler/FloatHandler.php new file mode 100644 index 00000000..33fb1e3f --- /dev/null +++ b/bundle/Form/FieldTypeHandler/FloatHandler.php @@ -0,0 +1,77 @@ +fieldHelper = $fieldHelper; + } + + /** + * @param \Ibexa\Core\FieldType\Float\Value $value + */ + public function convertFieldValueToForm(Value $value, ?FieldDefinition $fieldDefinition = null): float + { + return $value->value; + } + + public function convertFieldValueFromForm(mixed $data): FloatValue + { + if (!is_numeric($data)) { + $data = null; + } + + return new FloatValue($data); + } + + protected function buildFieldForm( + FormBuilderInterface $formBuilder, + FieldDefinition $fieldDefinition, + string $languageCode, + ?Content $content = null + ): void { + $options = $this->getDefaultFieldOptions($fieldDefinition, $languageCode, $content); + + if (!empty($fieldDefinition->getValidatorConfiguration()['FloatValueValidator'])) { + $rangeConstraints = []; + + $min = $fieldDefinition->getValidatorConfiguration()['FloatValueValidator']['minFloatValue']; + $max = $fieldDefinition->getValidatorConfiguration()['FloatValueValidator']['maxFloatValue']; + + if ($min !== false) { + $rangeConstraints['min'] = $min; + } + + if ($max !== false) { + $rangeConstraints['max'] = $max; + } + + if (!empty($rangeConstraints)) { + $options['constraints'][] = new Assert\Range($rangeConstraints); + } + } + + if (!$content instanceof Content && $fieldDefinition->defaultValue instanceof FloatValue) { + $options['data'] = (float) $fieldDefinition->defaultValue->value; + } + + $formBuilder->add($fieldDefinition->identifier, NumberType::class, $options); + } +} diff --git a/bundle/Form/FieldTypeHandler/Image.php b/bundle/Form/FieldTypeHandler/Image.php new file mode 100644 index 00000000..7e117879 --- /dev/null +++ b/bundle/Form/FieldTypeHandler/Image.php @@ -0,0 +1,59 @@ + $data->getRealPath(), + 'fileName' => $data->getClientOriginalName(), + 'fileSize' => $data->getSize(), + ]; + + return new ImageValue($imageData); + } + + protected function buildFieldForm( + FormBuilderInterface $formBuilder, + FieldDefinition $fieldDefinition, + string $languageCode, + ?Content $content = null + ): void { + $options = $this->getDefaultFieldOptions($fieldDefinition, $languageCode, $content); + + $maxFileSize = $fieldDefinition->validatorConfiguration['FileSizeValidator']['maxFileSize'] ?? false; + + if ($maxFileSize !== false) { + $options['constraints'][] = new Constraints\File( + [ + 'maxSize' => $maxFileSize, + ] + ); + } + + $options['block_name'] = 'ibexa_forms_image'; + + $formBuilder->add($fieldDefinition->identifier, FileType::class, $options); + } +} diff --git a/bundle/Form/FieldTypeHandler/IntegerHandler.php b/bundle/Form/FieldTypeHandler/IntegerHandler.php new file mode 100644 index 00000000..0e79a17b --- /dev/null +++ b/bundle/Form/FieldTypeHandler/IntegerHandler.php @@ -0,0 +1,77 @@ +fieldHelper = $fieldHelper; + } + + /** + * @param \Ibexa\Core\FieldType\Integer\Value $value + */ + public function convertFieldValueToForm(Value $value, ?FieldDefinition $fieldDefinition = null): int + { + return (int) $value->value; + } + + public function convertFieldValueFromForm(mixed $data): IntegerValue + { + if (!is_int($data)) { + $data = null; + } + + return new IntegerValue($data); + } + + protected function buildFieldForm( + FormBuilderInterface $formBuilder, + FieldDefinition $fieldDefinition, + string $languageCode, + ?Content $content = null + ): void { + $options = $this->getDefaultFieldOptions($fieldDefinition, $languageCode, $content); + + if (!$content instanceof Content && $fieldDefinition->defaultValue instanceof IntegerValue) { + $options['data'] = (int) $fieldDefinition->defaultValue->value; + } + + if (!empty($fieldDefinition->getValidatorConfiguration()['IntegerValueValidator'])) { + $rangeConstraints = []; + + $min = $fieldDefinition->getValidatorConfiguration()['IntegerValueValidator']['minIntegerValue']; + $max = $fieldDefinition->getValidatorConfiguration()['IntegerValueValidator']['maxIntegerValue']; + + if ($min !== null) { + $rangeConstraints['min'] = $min; + } + + if ($max !== null) { + $rangeConstraints['max'] = $max; + } + + if (!empty($rangeConstraints)) { + $options['constraints'][] = new Assert\Range($rangeConstraints); + } + } + + $formBuilder->add($fieldDefinition->identifier, IntegerType::class, $options); + } +} diff --git a/bundle/Form/FieldTypeHandler/Isbn.php b/bundle/Form/FieldTypeHandler/Isbn.php new file mode 100644 index 00000000..a3c9a460 --- /dev/null +++ b/bundle/Form/FieldTypeHandler/Isbn.php @@ -0,0 +1,55 @@ +isbn; + } + + public function convertFieldValueFromForm(mixed $data): IsbnValue + { + if (empty($data)) { + $data = ''; + } + + return new IsbnValue($data); + } + + protected function buildFieldForm( + FormBuilderInterface $formBuilder, + FieldDefinition $fieldDefinition, + string $languageCode, + ?Content $content = null + ): void { + $options = $this->getDefaultFieldOptions($fieldDefinition, $languageCode, $content); + + if ($fieldDefinition->fieldSettings['isISBN13'] ?? true) { + $options['constraints'][] = new Constraints\Isbn( + [ + 'type' => 'isbn13', + ] + ); + } else { + $options['constraints'][] = new Constraints\Isbn(); + } + + $formBuilder->add($fieldDefinition->identifier, TextType::class, $options); + } +} diff --git a/bundle/Form/FieldTypeHandler/MapLocation.php b/bundle/Form/FieldTypeHandler/MapLocation.php new file mode 100644 index 00000000..254bf057 --- /dev/null +++ b/bundle/Form/FieldTypeHandler/MapLocation.php @@ -0,0 +1,54 @@ + empty($value->latitude) ? null : $value->latitude, + 'longitude' => empty($value->longitude) ? null : $value->longitude, + 'address' => empty($value->address) ? null : $value->address, + ]; + } + + public function convertFieldValueFromForm(mixed $data): ?MapLocationValue + { + if (!is_array($data)) { + return null; + } + + return new MapLocationValue( + [ + 'latitude' => $data['latitude'], + 'longitude' => $data['longitude'], + 'address' => $data['address'], + ] + ); + } + + protected function buildFieldForm( + FormBuilderInterface $formBuilder, + FieldDefinition $fieldDefinition, + string $languageCode, + ?Content $content = null + ): void { + $options = $this->getDefaultFieldOptions($fieldDefinition, $languageCode, $content); + + $options['block_name'] = 'ibexa_forms_map'; + + $formBuilder->add($fieldDefinition->identifier, MapType::class, $options); + } +} diff --git a/bundle/Form/FieldTypeHandler/Relation.php b/bundle/Form/FieldTypeHandler/Relation.php new file mode 100644 index 00000000..1915a588 --- /dev/null +++ b/bundle/Form/FieldTypeHandler/Relation.php @@ -0,0 +1,66 @@ +repository = $repository; + } + + /** + * @param \Ibexa\Core\FieldType\Relation\Value $value + */ + public function convertFieldValueToForm(Value $value, ?FieldDefinition $fieldDefinition = null): mixed + { + if (empty($value->destinationContentId)) { + return null; + } + + return $value->destinationContentId; + } + + protected function buildFieldForm( + FormBuilderInterface $formBuilder, + FieldDefinition $fieldDefinition, + string $languageCode, + ?Content $content = null + ): void { + $selectionRoot = $fieldDefinition->getFieldSettings()['selectionRoot']; + + if (empty($selectionRoot)) { + throw new InvalidArgumentException('SelectionRoot must be defined'); + } + + $locationService = $this->repository->getLocationService(); + $location = $locationService->loadLocation($selectionRoot); + $locationList = $locationService->loadLocationChildren($location); + + $choices = []; + foreach ($locationList->locations as $child) { + /* @var Location $child */ + $choices[$child->getContent()->getName()] = $child->contentInfo->id; + } + + $formBuilder->add($fieldDefinition->identifier, ChoiceType::class, [ + 'choices' => $choices, + 'expanded' => false, + 'multiple' => false, + ]); + } +} diff --git a/bundle/Form/FieldTypeHandler/RelationList.php b/bundle/Form/FieldTypeHandler/RelationList.php new file mode 100644 index 00000000..614a99b9 --- /dev/null +++ b/bundle/Form/FieldTypeHandler/RelationList.php @@ -0,0 +1,104 @@ +repository = $repository; + } + + public function convertFieldValueToForm(Value $value, ?FieldDefinition $fieldDefinition = null): ?array + { + if (empty($value->destinationContentIds)) { + return null; + } + + /** @var $value \Ibexa\Core\FieldType\RelationList\Value */ + return $value->destinationContentIds; + } + + public function convertFieldValueFromForm(mixed $data): RelationListValue + { + return new RelationListValue($data); + } + + protected function buildFieldForm( + FormBuilderInterface $formBuilder, + FieldDefinition $fieldDefinition, + string $languageCode, + ?Content $content = null + ): void { + $options = $this->getDefaultFieldOptions($fieldDefinition, $languageCode, $content); + + $fieldSettings = $fieldDefinition->getFieldSettings(); + + $selectionMethod = $fieldSettings['selectionMethod']; + + $defaultLocation = $fieldSettings['selectionDefaultLocation']; + $contentTypes = $fieldSettings['selectionContentTypes']; + + /* TODO: implement different selection methods */ + switch ($fieldSettings['selectionMethod']) { + case self::MULTIPLE_SELECTION: + $locationService = $this->repository->getLocationService(); + $location = $locationService->loadLocation($defaultLocation ?: 2); + $locationList = $locationService->loadLocationChildren($location); + + $choices = []; + + foreach ($locationList->locations as $child) { + $choices[$child->getContent()->getName()] = $child->contentInfo->id; + } + + $formBuilder->add($fieldDefinition->identifier, ChoiceType::class, [ + 'choices' => $choices, + 'expanded' => false, + 'multiple' => true, + ] + $options); + + break; + + default: + $locationService = $this->repository->getLocationService(); + $location = $locationService->loadLocation($defaultLocation ?: 2); + $locationList = $locationService->loadLocationChildren($location); + + $choices = []; + + foreach ($locationList->locations as $child) { + $choices[$child->getContent()->getName()] = $child->contentInfo->id; + } + + $formBuilder->add($fieldDefinition->identifier, ChoiceType::class, [ + 'choices' => $choices, + 'expanded' => false, + 'multiple' => false, + ] + $options); + + break; + } + } +} diff --git a/bundle/Form/FieldTypeHandler/Selection.php b/bundle/Form/FieldTypeHandler/Selection.php new file mode 100644 index 00000000..e5cddea6 --- /dev/null +++ b/bundle/Form/FieldTypeHandler/Selection.php @@ -0,0 +1,62 @@ +getFieldSettings(); + $isMultiple = $fieldSettings['isMultiple']; + } + + if (!$isMultiple) { + if (empty($value->selection)) { + return ''; + } + + return $value->selection[0]; + } + + return $value->selection; + } + + protected function buildFieldForm( + FormBuilderInterface $formBuilder, + FieldDefinition $fieldDefinition, + string $languageCode, + ?Content $content = null + ): void { + $options = $this->getDefaultFieldOptions($fieldDefinition, $languageCode, $content); + + $values = $fieldDefinition->getFieldSettings()['options']; + + $options['expanded'] = false; + $options['multiple'] = $fieldDefinition->getFieldSettings()['isMultiple']; + + $options['choices'] = array_flip($values); + + $formBuilder->add($fieldDefinition->identifier, ChoiceType::class, $options); + } +} diff --git a/bundle/Form/FieldTypeHandler/TextBlock.php b/bundle/Form/FieldTypeHandler/TextBlock.php new file mode 100644 index 00000000..419e7f10 --- /dev/null +++ b/bundle/Form/FieldTypeHandler/TextBlock.php @@ -0,0 +1,46 @@ +text; + } + + public function convertFieldValueFromForm(mixed $data): TextBlockValue + { + if (empty($data)) { + $data = ''; + } + + return new TextBlockValue($data); + } + + protected function buildFieldForm( + FormBuilderInterface $formBuilder, + FieldDefinition $fieldDefinition, + string $languageCode, + ?Content $content = null + ): void { + $options = $this->getDefaultFieldOptions($fieldDefinition, $languageCode, $content); + + $options['attr']['rows'] = $fieldDefinition->fieldSettings['textRows']; + + $formBuilder->add($fieldDefinition->identifier, TextareaType::class, $options); + } +} diff --git a/bundle/Form/FieldTypeHandler/TextLine.php b/bundle/Form/FieldTypeHandler/TextLine.php new file mode 100644 index 00000000..12481deb --- /dev/null +++ b/bundle/Form/FieldTypeHandler/TextLine.php @@ -0,0 +1,64 @@ +text; + } + + public function convertFieldValueFromForm(mixed $data): TextLineValue + { + if (empty($data)) { + $data = ''; + } + + return new TextLineValue($data); + } + + protected function buildFieldForm( + FormBuilderInterface $formBuilder, + FieldDefinition $fieldDefinition, + string $languageCode, + ?Content $content = null + ): void { + $options = $this->getDefaultFieldOptions($fieldDefinition, $languageCode, $content); + + if (!empty($fieldDefinition->validatorConfiguration['StringLengthValidator'])) { + $lengthConstraints = []; + + $minStringLength = $fieldDefinition->validatorConfiguration['StringLengthValidator']['minStringLength']; + $maxStringLength = $fieldDefinition->validatorConfiguration['StringLengthValidator']['maxStringLength']; + + if (!empty($minStringLength)) { + $lengthConstraints['min'] = $minStringLength; + } + + if (!empty($maxStringLength)) { + $lengthConstraints['max'] = $maxStringLength; + } + + if (!empty($lengthConstraints)) { + $options['constraints'][] = new Constraints\Length($lengthConstraints); + } + } + + $formBuilder->add($fieldDefinition->identifier, TextType::class, $options); + } +} diff --git a/bundle/Form/FieldTypeHandler/Time.php b/bundle/Form/FieldTypeHandler/Time.php new file mode 100644 index 00000000..7531d944 --- /dev/null +++ b/bundle/Form/FieldTypeHandler/Time.php @@ -0,0 +1,63 @@ +time; + if (is_int($time)) { + return new DateTime("@{$time}"); + } + + return new DateTime(); + } + + public function convertFieldValueFromForm(mixed $data): TimeValue + { + if ($data instanceof DateTime) { + return TimeValue::fromDateTime($data); + } + + if (is_int($data)) { + return new TimeValue($data); + } + + return new TimeValue(null); + } + + protected function buildFieldForm( + FormBuilderInterface $formBuilder, + FieldDefinition $fieldDefinition, + string $languageCode, + ?Content $content = null + ): void { + $options = $this->getDefaultFieldOptions($fieldDefinition, $languageCode, $content); + + $useSeconds = $fieldDefinition->getFieldSettings()['useSeconds']; + $options['input'] = 'datetime'; + $options['widget'] = 'choice'; + $options['with_seconds'] = $useSeconds; + $options['constraints'][] = new Assert\Time(); + + $formBuilder->add($fieldDefinition->identifier, TimeType::class, $options); + } +} diff --git a/bundle/Form/FieldTypeHandler/Url.php b/bundle/Form/FieldTypeHandler/Url.php new file mode 100644 index 00000000..b64a2383 --- /dev/null +++ b/bundle/Form/FieldTypeHandler/Url.php @@ -0,0 +1,47 @@ + $value->link, 'text' => $value->text]; + } + + public function convertFieldValueFromForm(mixed $data): UrlValue + { + if (!is_array($data)) { + $data = []; + $data['url'] = null; + $data['text'] = null; + } + + return new UrlValue($data['url'], $data['text']); + } + + protected function buildFieldForm( + FormBuilderInterface $formBuilder, + FieldDefinition $fieldDefinition, + string $languageCode, + ?Content $content = null + ): void { + $options = $this->getDefaultFieldOptions($fieldDefinition, $languageCode, $content); + + $formBuilder->add($fieldDefinition->identifier, UrlType::class, $options); + } +} diff --git a/bundle/Form/FieldTypeHandlerInterface.php b/bundle/Form/FieldTypeHandlerInterface.php new file mode 100644 index 00000000..fac828fd --- /dev/null +++ b/bundle/Form/FieldTypeHandlerInterface.php @@ -0,0 +1,53 @@ +map = $map; + } + + /** + * Register a $service for FieldType $identifier. + */ + public function register(string $identifier, FieldTypeHandlerInterface $handler): void + { + $this->map[$identifier] = $handler; + } + + /** + * Returns a FieldTypeHandlerInterface for FieldType $identifier. + * + * @throws \OutOfBoundsException + * @throws \RuntimeException When type is not a FieldTypeHandlerInterface instance nor a callable factory + */ + public function get(string $identifier): FieldTypeHandlerInterface + { + if (!isset($this->map[$identifier])) { + throw new OutOfBoundsException("No handler registered for FieldType '{$identifier}'."); + } + if (!$this->map[$identifier] instanceof FieldTypeHandlerInterface) { + + $factory = $this->map[$identifier]; + $this->map[$identifier] = $factory(); + + if (!$this->map[$identifier] instanceof FieldTypeHandlerInterface) { + throw new RuntimeException( + "FieldTypeHandler '{$identifier}' callable did not return a FieldTypeHandlerInterface instance, " . + 'instead: ' . gettype($this->map[$identifier]) + ); + } + } + + return $this->map[$identifier]; + } +} diff --git a/bundle/Form/Payload/InformationCollectionStruct.php b/bundle/Form/Payload/InformationCollectionStruct.php new file mode 100644 index 00000000..374a609c --- /dev/null +++ b/bundle/Form/Payload/InformationCollectionStruct.php @@ -0,0 +1,37 @@ +collectedData[$fieldDefIdentifier] ?? null; + } + + /** + * This method returns the complete fields collection. + */ + public function getCollectedFields(): array + { + return $this->collectedData; + } + + /** + * Sets value for $fieldDefIdentifier. + */ + public function setCollectedFieldValue(string $fieldDefIdentifier, mixed $value): void + { + $this->collectedData[$fieldDefIdentifier] = $value; + } +} diff --git a/bundle/Form/Type/AbstractContentType.php b/bundle/Form/Type/AbstractContentType.php new file mode 100644 index 00000000..2c624ff2 --- /dev/null +++ b/bundle/Form/Type/AbstractContentType.php @@ -0,0 +1,22 @@ +fieldTypeHandlerRegistry = $fieldTypeHandlerRegistry; + $this->dataMapper = $dataMapper; + } +} diff --git a/bundle/Form/CaptchaType.php b/bundle/Form/Type/CaptchaType.php similarity index 96% rename from bundle/Form/CaptchaType.php rename to bundle/Form/Type/CaptchaType.php index 43de5d84..696a9deb 100644 --- a/bundle/Form/CaptchaType.php +++ b/bundle/Form/Type/CaptchaType.php @@ -1,6 +1,6 @@ configResolver = $configResolver; + } + + public function getName(): string + { + return $this->getBlockPrefix(); + } + + /** + * Returns the prefix of the template block name for this type. + */ + public function getBlockPrefix(): string + { + return 'ezforms_information_collection_update'; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired('collection'); + $resolver->setAllowedTypes('collection', Collection::class); + } + + /** + * {@inheritdoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + /** @var DataWrapper $dataWrapper */ + $dataWrapper = $options['data']; + + if (!$dataWrapper instanceof DataWrapper) { + throw new RuntimeException(sprintf('Data must be an instance of %s', DataWrapper::class)); + } + + /** @var InformationCollectionStruct $payload */ + $payload = $dataWrapper->payload; + + if (!$payload instanceof InformationCollectionStruct) { + throw new RuntimeException(sprintf('Data payload must be an instance of %s', InformationCollectionStruct::class)); + } + + /** @var ContentType $contentType */ + $contentType = $dataWrapper->definition; + + if (!$contentType instanceof ContentType) { + throw new RuntimeException(sprintf('Data definition must be an instance of %s', ContentType::class)); + } + + $builder->setDataMapper($this->dataMapper); + + foreach ($contentType->getFieldDefinitions() as $fieldDefinition) { + if ($fieldDefinition->fieldTypeIdentifier === 'ezuser') { + continue; + } + + if (!$fieldDefinition->isInfoCollector) { + continue; + } + + $handler = $this->fieldTypeHandlerRegistry->get($fieldDefinition->fieldTypeIdentifier); + + $handler->buildFieldUpdateForm( + $builder, + $fieldDefinition, + $dataWrapper->target->content, + $this->getLanguageCode($contentType) + ); + } + } + + /** + * If ContentType language code is in languages array then use it, else use the main language. + * + * @param ContentType $contentType + * + * @return string + */ + protected function getLanguageCode(ContentType $contentType): string + { + $contentTypeLanguages = array_keys($contentType->getNames()); + $languages = $this->configResolver->getParameter('languages'); + + foreach ($languages as $languageCode) { + if (in_array($languageCode, $contentTypeLanguages, true)) { + return $languageCode; + } + } + + return $contentType->mainLanguageCode; + } +} diff --git a/bundle/Form/Type/MapType.php b/bundle/Form/Type/MapType.php new file mode 100644 index 00000000..d5b33647 --- /dev/null +++ b/bundle/Form/Type/MapType.php @@ -0,0 +1,33 @@ +add('address', TextType::class, [ + 'label' => 'ibexa_forms.form.map.address.label', + ]); + + $builder->add('latitude', NumberType::class, [ + 'label' => 'ibexa_forms.form.map.latitude.label', + ]); + + $builder->add('longitude', NumberType::class, [ + 'label' => 'ibexa_forms.form.map.longitude.label', + ]); + } + + public function getBlockPrefix(): string + { + return 'ibexa_forms_map'; + } +} diff --git a/bundle/Form/Type/UrlType.php b/bundle/Form/Type/UrlType.php new file mode 100644 index 00000000..31a628a1 --- /dev/null +++ b/bundle/Form/Type/UrlType.php @@ -0,0 +1,32 @@ +add( + 'url', + CoreUrlType::class, + [ + 'constraints' => new Assert\Url(), + ] + ); + + $builder->add('text', TextType::class); + } + + public function getBlockPrefix(): string + { + return 'ibexa_forms_url'; + } +} diff --git a/bundle/Ibexa/ContentForms/InformationCollectionType.php b/bundle/Ibexa/ContentForms/InformationCollectionType.php index dcdeed6c..ab492408 100644 --- a/bundle/Ibexa/ContentForms/InformationCollectionType.php +++ b/bundle/Ibexa/ContentForms/InformationCollectionType.php @@ -5,7 +5,7 @@ namespace Netgen\Bundle\InformationCollectionBundle\Ibexa\ContentForms; use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface; -use Netgen\Bundle\InformationCollectionBundle\Form\CaptchaType; +use Netgen\Bundle\InformationCollectionBundle\Form\Type\CaptchaType; use Netgen\InformationCollection\API\Service\CaptchaService; use Netgen\InformationCollection\API\Value\InformationCollectionStruct; use Symfony\Component\Form\AbstractType; diff --git a/bundle/NetgenInformationCollectionBundle.php b/bundle/NetgenInformationCollectionBundle.php index b11eaedf..c638babe 100644 --- a/bundle/NetgenInformationCollectionBundle.php +++ b/bundle/NetgenInformationCollectionBundle.php @@ -3,6 +3,7 @@ namespace Netgen\Bundle\InformationCollectionBundle; use Ibexa\Bundle\Core\DependencyInjection\IbexaCoreExtension; +use Netgen\Bundle\InformationCollectionBundle\DependencyInjection\Compiler\FieldTypeHandlerRegistryPass; use Netgen\Bundle\InformationCollectionBundle\Ibexa\PolicyProvider\InformationCollectionPolicyProvider; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -11,6 +12,7 @@ class NetgenInformationCollectionBundle extends Bundle { public function build(ContainerBuilder $container): void { + $container->addCompilerPass(new FieldTypeHandlerRegistryPass()); parent::build($container); $ibexaExtension = $container->getExtension('ibexa'); diff --git a/bundle/Resources/config/controllers.yml b/bundle/Resources/config/controllers.yml index bb45643b..073bd430 100644 --- a/bundle/Resources/config/controllers.yml +++ b/bundle/Resources/config/controllers.yml @@ -17,6 +17,10 @@ services: - "@ibexa.api.service.content" - "@ibexa.config.resolver" - "@translator" + - '@netgen_information_collection.factory.field_data' + - '@netgen_information_collection.repository.ez_info_collection' + - '@netgen_information_collection.repository.ez_info_collection_attribute' + - '@netgen_information_collection.form.builder' netgen_information_collection.controller.export.export: class: Netgen\Bundle\InformationCollectionBundle\Controller\Admin\Export\Export diff --git a/bundle/Resources/config/routing.yaml b/bundle/Resources/config/routing.yaml index b3a25989..9f138568 100644 --- a/bundle/Resources/config/routing.yaml +++ b/bundle/Resources/config/routing.yaml @@ -18,6 +18,11 @@ netgen_information_collection.route.admin.view: defaults: { _controller: netgen_information_collection.controller.admin:viewAction } methods: [GET] +netgen_information_collection.route.admin.edit: + path: /netgen/informationcollection/edit/{collectionId} + defaults: { _controller: netgen_information_collection.controller.admin:editAction } + methods: [ GET, POST ] + netgen_information_collection.route.admin.handle_contents: path: /netgen/informationcollection/handle/contents defaults: { _controller: netgen_information_collection.controller.admin:handleContentsAction } diff --git a/bundle/Resources/config/services.yml b/bundle/Resources/config/services.yml index d051d4b0..211b9ec8 100644 --- a/bundle/Resources/config/services.yml +++ b/bundle/Resources/config/services.yml @@ -32,3 +32,14 @@ services: - '@request_stack' - '@netgen_information_collection.captcha.service' - '@translator' + + netgen_information_collection.form.builder: + class: Netgen\Bundle\InformationCollectionBundle\Form\Builder\FormBuilder + arguments: + - '@form.factory' + - '@ibexa.api.service.content_type' + - '@router' + - '@ibexa.config.resolver' + - '@netgen_information_collection.factory.field_data' + - '@netgen_information_collection.field_handler.registry' + diff --git a/bundle/Resources/public/admin/css/ic_forms.css b/bundle/Resources/public/admin/css/ic_forms.css new file mode 100644 index 00000000..c2cc8839 --- /dev/null +++ b/bundle/Resources/public/admin/css/ic_forms.css @@ -0,0 +1,81 @@ + .ic-content { + margin-bottom: 2em; + } + .ic-content .row-input { + margin-bottom: 1em; + } + .ic-content label { + display: block; + font-weight: normal; + font-size: 14px; + margin: 0; + } + .ic-content input[type=text], + .ic-content input[type=number], + .ic-content input[type=url], + .ic-content input[type=email], + .ic-content textarea, + .ic-content select { + display: block; + margin: 0 0 1em; + border-radius: 2px; + width: 40%; + padding: 0 0.75em; + font-size: 14px; + height: 44px; + border: 1px solid #b3b3b3; + background: #fff; + } + .ic-content input[type=text]::placeholder, + .ic-content input[type=number]::placeholder, + .ic-content input[type=url]::placeholder, + .ic-content input[type=email]::placeholder, + .ic-content textarea::placeholder, + .ic-content select::placeholder { + color: #b3b3b3; + } + .ic-content input[type=text][disabled], + .ic-content input[type=number][disabled], + .ic-content input[type=url][disabled], + .ic-content input[type=email][disabled], + .ic-content textarea[disabled], + .ic-content select[disabled] { + color: #999999; + border-color: #cccccc; + cursor: not-allowed; + } + .ic-content select[multiple] { + background-image: none; + height: auto; + padding: 0.25em 0; + } + .ic-content select[multiple] option { + padding: 0.25em 0.5em; + } + .ic-content textarea { + height: auto; + min-height: 120px; + padding-top: 0.375em; + resize: vertical; + } + .ic-content .file-input-group .filename { + margin-left: 1em; + font-size: 0.875em; + } + .ic-content .birthday div { + display: flex; + } + .ic-content .birthday div select { + width: 13%; + margin-right: 7px; + } + .ic-content .error-input ul { + margin-left: 0; + } + .ic-content .error-input ul li { + list-style-type: none; + color: red; + } + .ic-content form div { + padding: 8px 0; + } \ No newline at end of file diff --git a/bundle/Resources/translations/netgen_information_collection_admin.en.yml b/bundle/Resources/translations/netgen_information_collection_admin.en.yml index aa20034e..fbe33836 100644 --- a/bundle/Resources/translations/netgen_information_collection_admin.en.yml +++ b/bundle/Resources/translations/netgen_information_collection_admin.en.yml @@ -58,3 +58,6 @@ netgen_information_collection_admin_export_no_formatters: 'No formatters found' netgen_information_collection_admin_collected_information: 'Collected information (%count%)' netgen_information_collection_admin_missing_location: 'Content does not have a valid location.' + +netgen_information_collection_admin_collection_edit: 'Edit' +netgen_information_collection_admin_collection_save: 'Save' diff --git a/bundle/Resources/views/admin/collection_list.html.twig b/bundle/Resources/views/admin/collection_list.html.twig index d550aeae..8be80c82 100644 --- a/bundle/Resources/views/admin/collection_list.html.twig +++ b/bundle/Resources/views/admin/collection_list.html.twig @@ -56,6 +56,7 @@