fix: deactivable services and child groups (#739)
This commit is contained in:
parent
951ebf9ab6
commit
97a839c491
14 changed files with 181 additions and 14 deletions
|
|
@ -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
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in a new issue