fix: deactivable services and child groups (#739)

This commit is contained in:
JacquesDurand 2024-09-20 09:53:00 +02:00 committed by Hugo Nicolas
parent 951ebf9ab6
commit 97a839c491
No known key found for this signature in database
GPG key ID: 09CB3D93EB8B0E61
14 changed files with 181 additions and 14 deletions

View file

@ -4,6 +4,17 @@ import { Controller} from '@hotwired/stimulus'
export default class extends Controller {
static targets = ['servicesEnabledField', 'parentField', 'idField']
connect() {
const parentFields = document.querySelectorAll('[data-label="Parent"]')
const trs = Array.from(parentFields)
.map(e => e.firstElementChild)
.filter(e => e.tagName === 'A')
.map(e => e.closest('tr'))
for (const tr of trs) {
this.checkDisabledServices(tr)
}
}
parentFieldTargetConnected(element) {
const observer = new MutationObserver( ( ) => {
if (element.tomselect) {
@ -43,9 +54,45 @@ export default class extends Controller {
const params = new URLSearchParams(this.servicesEnabledFieldTarget.getAttribute('data-toggle-url'))
this.disableServicesForChildGroups(params.get('entityId'))
}
const parentFields = document.querySelectorAll('[data-label="Parent"]')
const trs = Array.from(parentFields)
.map(e => e.firstElementChild)
.filter(e => e.tagName === 'A')
.map(e => e.closest('tr'))
for (const tr of trs) {
this.checkDisabledServices(tr)
}
})
}
async checkDisabledServices(tr) {
const id = tr.getAttribute('data-id')
const url = `/api/groups/${id}`
const response = await fetch(url, {method: 'GET'})
if (!response.ok) {
return
}
const group = await response.json()
let disabledTr = false
for (const parentUrl of group.parentsRecursively) {
const parentResponse = await fetch(parentUrl, { method: 'GET' })
if (!response.ok) {
return
}
const parent = await parentResponse.json()
if (!parent.servicesEnabled) {
tr.querySelector('[data-admin-parentgroup-target="servicesEnabledField"]').disabled = true
disabledTr = true
break
}
}
if (!disabledTr) {
tr.querySelector('[data-admin-parentgroup-target="servicesEnabledField"]').disabled = false
}
}
async disableServicesForChildGroups(groupId) {
const url = `/api/groups/${groupId}/disable_child_services`
@ -59,7 +106,7 @@ export default class extends Controller {
return
}
const data = await response.json()
const groupChild = data.children
const groupChild = data.childrenRecursively
const groupChildId = groupChild.map(group => {
return group.split('/')[3]
@ -70,6 +117,7 @@ export default class extends Controller {
const params = new URLSearchParams(toggle.getAttribute('data-toggle-url'))
if(groupChildId.includes(params.get('entityId'))) {
toggle.checked = false
toggle.disabled = true
}
})

View file

@ -8,6 +8,7 @@ App\Entity\Product:
service (template, extends product):
type: !php/enum App\Enum\Product\ProductType::SERVICE
visibility: !php/enum App\Enum\Product\ProductVisibility::RESTRICTED
# Loic —————————————————————————————————————————————————————————————————————
loic_object_1 (extends object):

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Controller\Admin;
use App\Controller\i18nTrait;
use App\EasyAdmin\Field\FieldTrait;
use App\EasyAdmin\Filter\EnumFilter;
use App\EasyAdmin\Filter\UuidFilter;
@ -15,8 +16,10 @@ use App\Enum\Product\ProductType;
use App\Enum\Product\ProductVisibility;
use App\Flysystem\EasyAdminHelper;
use App\Flysystem\MediaManager;
use App\Form\Type\Product\AbstractProductFormType;
use App\Helper\CsvExporter;
use App\Repository\CategoryRepository;
use App\Repository\GroupRepository;
use App\Repository\ProductRepository;
use Doctrine\ORM\QueryBuilder;
use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection;
@ -54,6 +57,7 @@ use Symfony\Contracts\Translation\TranslatorInterface;
abstract class AbstractProductCrudController extends AbstractCrudController implements AdminSecuredCrudControllerInterface
{
use FieldTrait;
use i18nTrait;
abstract public function getProductType(): ProductType;
@ -74,6 +78,7 @@ abstract class AbstractProductCrudController extends AbstractCrudController impl
private readonly TranslatorInterface $translator,
private readonly FilterFactory $filterFactory,
private readonly SluggerInterface $slugger,
protected readonly GroupRepository $groupRepository,
) {
}
@ -88,6 +93,9 @@ abstract class AbstractProductCrudController extends AbstractCrudController impl
'@EasyAdmin/crud/form_theme.html.twig',
'easy_admin/crud/form_theme.html.twig',
])
->setFormOptions([
'validation_groups' => [AbstractProductFormType::class],
])
;
}
@ -241,7 +249,8 @@ abstract class AbstractProductCrudController extends AbstractCrudController impl
->setFormType(EnumType::class)
->setFormTypeOption('class', ProductVisibility::class)
->setChoices(ProductVisibility::getAsArray());
$groupsField = CollectionField::new('groups');
$groupsField = AssociationField::new('groups')->onlyOnForms();
$groupsFieldList = CollectionField::new('groups')->hideOnForm();
$ownerField = AssociationField::new('owner');
$categoryField = AssociationField::new('category')
@ -300,6 +309,7 @@ abstract class AbstractProductCrudController extends AbstractCrudController impl
'statusField',
'visibilityField',
'groupsField',
'groupsFieldList',
'ownerField',
'categoryField',
'nameField',

View file

@ -291,6 +291,18 @@ final class GroupCrudController extends AbstractCrudController implements GroupA
];
if ($this->configurationRepository->getInstanceConfigurationOrCreate()->getServicesEnabled()) {
$i18prefix = $this->getI18nPrefix(self::class);
/** @var Group|null $group */
$group = $this->getContext()?->getEntity()?->getInstance();
if (null !== $group) {
foreach ($group->getParentsRecursively() as $parent) {
if (!$parent->getServicesEnabled()) {
$servicesEnabledField->setDisabled();
$servicesEnabledField->setHelp($i18prefix.'.field.services_enabled.parent_disabled');
break;
}
}
}
array_splice($fields, 3, 0, [$servicesEnabledField]);
}

View file

@ -5,10 +5,13 @@ declare(strict_types=1);
namespace App\Controller\Admin;
use App\Entity\Product;
use App\Enum\Group\UserMembership;
use App\Enum\Product\ProductStatus;
use App\Enum\Product\ProductType;
use App\Enum\Product\ProductVisibility;
use Doctrine\ORM\QueryBuilder;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField;
use EasyCorp\Bundle\EasyAdminBundle\Field\ImageField;
@ -36,6 +39,7 @@ final class ServiceCrudController extends AbstractProductCrudController
{
$product = parent::createEntity($entityFqcn);
$product->setCurrency(null); // remove the default value which is not needed here
$product->setVisibility(ProductVisibility::RESTRICTED);
return $product;
}
@ -49,6 +53,8 @@ final class ServiceCrudController extends AbstractProductCrudController
'typeField' => $typeField,
'statusField' => $statusField,
'visibilityField' => $visibilityField,
'groupsField' => $groupsField,
'groupsFieldList' => $groupsFieldList,
'ownerField' => $ownerField,
'categoryField' => $categoryField,
'nameField' => $nameField,
@ -61,7 +67,7 @@ final class ServiceCrudController extends AbstractProductCrudController
// list
if ($pageName === Crud::PAGE_INDEX) {
return [$nameField, $ownerField, $categoryField, $statusField, $visibilityField, $imageField, $createdAt];
return [$nameField, $ownerField, $categoryField, $statusField, $visibilityField, $groupsFieldList, $imageField, $createdAt];
}
/** @var ImageField $imageField */
@ -71,10 +77,33 @@ final class ServiceCrudController extends AbstractProductCrudController
if ($pageName === Crud::PAGE_NEW || $pageName === Crud::PAGE_EDIT) {
/** @var ChoiceField $statusField */
$statusField->setChoices(ProductStatus::cases());
/** @var ChoiceField $visibilityField */
$visibilityField->setChoices(ProductVisibility::cases());
return [$nameField, $ownerField, $categoryField, $statusField, $visibilityField, $descriptionField, $imageField, $durationField];
if ($pageName === Crud::PAGE_NEW) {
return [$nameField, $ownerField, $categoryField, $statusField, $groupsField, $descriptionField, $imageField, $durationField];
}
/** @var Product|null $product */
$product = $this->getContext()?->getEntity()?->getInstance();
$owner = $product?->getOwner();
if (null !== $owner && !$owner->getUserGroupsConfirmedWithServices()->isEmpty()) {
/** @var AssociationField $groupsField */
$groupsField->setQueryBuilder(function (QueryBuilder $queryBuilder) use ($owner) {
return $queryBuilder
->join('entity.userGroups', 'ug')
->andWhere('ug.membership != :membership')
->andWhere('ug.user = :user')
->andWhere('entity.servicesEnabled = :true')
->setParameter('user', $owner)
->setParameter('membership', UserMembership::INVITATION)
->setParameter('true', true)
;
});
} else {
$i18prefix = $this->getI18nPrefix(self::class);
/** @var AssociationField $groupsField */
$groupsField->setHelp($i18prefix.'.field.groups.help')->setDisabled();
}
return [$nameField, $ownerField, $categoryField, $statusField, $groupsField, $descriptionField, $imageField, $durationField];
}
// detail

View file

@ -8,6 +8,7 @@ use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use App\Doctrine\Behavior\TimestampableEntity;
@ -39,6 +40,7 @@ use Symfony\Component\Validator\Constraints as Assert;
input: false,
processor: GroupChildServicesEnabledProcessor::class
),
new Get(),
]
)]
class Group implements \Stringable
@ -412,4 +414,34 @@ class Group implements \Stringable
{
return !$this->getActiveOffers()->isEmpty();
}
/**
* @return list<Group>
*/
public function getParentsRecursively(): array
{
$parents = [];
$parent = $this->getParent();
if (null !== $parent) {
$parents = $parent->getParentsRecursively();
$parents[] = $parent;
}
return $parents;
}
/**
* @return list<Group>
*/
public function getChildrenRecursively(): array
{
$result = [];
$children = $this->getChildren();
foreach ($children as $child) {
$result = array_merge($result, $child->getChildrenRecursively());
$result[] = $child;
}
return $result;
}
}

View file

@ -95,7 +95,7 @@ abstract class AbstractProductFormType extends AbstractType
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'validation_groups' => self::class,
'validation_groups' => [self::class],
]);
}
}

View file

@ -44,8 +44,8 @@ final class ServiceFormType extends AbstractProductFormType
]);
// only if the user is connected and has groups
$groups = $this->groupRepository->getUserGroupsWithEnabledServices($user);
if (!$user->getUserGroupsConfirmed()->isEmpty()) {
$groups = $this->groupRepository->getUserGroupsWithEnabledServices($user);
$builder
->add('visibility', ChoiceType::class, [
'label' => 'product.form.visibility',
@ -66,11 +66,15 @@ final class ServiceFormType extends AbstractProductFormType
'class' => Group::class,
'query_builder' => $groups,
'label' => [] === $groups->getQuery()->getResult() ? $i18nPrefix.'.form.no_groups' : $i18nPrefix.'.form.groups',
'label_attr' => [] === $groups->getQuery()->getResult() ? ['class' => 'text-danger fs-6 fw-normal'] : [],
'expanded' => true,
'multiple' => true,
'required' => false,
]);
}
if ($groups->getQuery()->getResult() === []) {
$builder->get('submit')->setDisabled(true);
}
}
public function getType(): ProductType

View file

@ -126,8 +126,7 @@ final class GroupRepository extends ServiceEntityRepository
public function disableServicesForChildGroup(Group $group): void
{
/** @var Group $child */
foreach ($group->getChildren() as $child) {
foreach ($group->getChildrenRecursively() as $child) {
$child->setServicesEnabled(false);
$this->getEntityManager()->persist($child);
}

View file

@ -42,7 +42,6 @@ final class ServiceCrudControllerTest extends WebTestCase
$form = $crawler->selectButton(TestReference::ACTION_SAVE_AND_RETURN)->form();
$client->submit($form, [
$form->getName().'[name]' => 'Object public',
$form->getName().'[visibility]' => 'public',
$form->getName().'[status]' => 'active',
$form->getName().'[owner]' => TestReference::ADMIN_LOIC,
$form->getName().'[description]' => 'very nice object',

View file

@ -45,13 +45,14 @@ final class IndexProductsCommandTest extends KernelTestCase
$searchDto = new Search('');
// all documents when not logged (-1 because of a restricted product)
// services are now restricted and do not appear here with default filters
$results = $meilisearch->search($searchDto);
self::assertSame(TestReference::PRODUCTS_INDEXABLE_COUNT - 1, $results->getHitsCount());
self::assertSame(TestReference::PRODUCTS_VISIBLE_COUNT - 1, $results->getHitsCount());
// all documents when logged with a user with access to the restricted product
$searchDto->user = $this->getUserRepository()->get(TestReference::PLACE_APES);
$results = $meilisearch->search($searchDto);
self::assertSame(TestReference::PRODUCTS_INDEXABLE_COUNT, $results->getHitsCount());
self::assertSame(TestReference::PRODUCTS_VISIBLE_COUNT, $results->getHitsCount());
// keyword search
$searchDto->user = null;
@ -60,7 +61,7 @@ final class IndexProductsCommandTest extends KernelTestCase
self::assertSame(3, $results->getHitsCount());
// typo tolerance example
$searchDto->q = 'histiore';
$searchDto->q = 'jumeles';
$results = $meilisearch->search($searchDto);
self::assertSame(1, $results->getHitsCount());
}

View file

@ -69,6 +69,8 @@ final class TestReference
public const PRODUCTS_COUNT = 18;
public const PRODUCTS_NOT_INDEXABLE_COUNT = 1;
public const PRODUCTS_INDEXABLE_COUNT = self::PRODUCTS_COUNT - self::PRODUCTS_NOT_INDEXABLE_COUNT;
public const PRODUCTS_RESTRICTED_COUNT = 5;
public const PRODUCTS_VISIBLE_COUNT = 12;
public const PRODUCT_AVAILABILITIES_COUNT = 4;
public const USER_17_SERVICES_COUNT = 0;

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="fr" datatype="plaintext" original="file.ext">
<header>
<tool tool-id="symfony" tool-name="Symfony" />
</header>
<body>
<trans-unit id="tN4hAc3" resname="app.controller.admin.group_crud_controller.field.services_enabled.parent_disabled">
<source>app.controller.admin.group_crud_controller.field.services_enabled.parent_disabled</source>
<target>Un des groupes parents de ce groupe a désactivé la disponibilité des services</target>
</trans-unit>
</body>
</file>
</xliff>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en" target-language="fr" datatype="plaintext" original="file.ext">
<header>
<tool tool-id="symfony" tool-name="Symfony" />
</header>
<body>
<trans-unit id="3yKyMcK" resname="app.controller.admin.service_crud_controller.field.groups.help">
<source>app.controller.admin.service_crud_controller.field.groups.help</source>
<target>Aucun des groupes du/de la propriétaire n'a la disponibilité des services activée</target>
</trans-unit>
</body>
</file>
</xliff>