diff --git a/databox/api/src/Api/Model/Output/ShareAlternateUrlOutput.php b/databox/api/src/Api/Model/Output/ShareAlternateUrlOutput.php index ed131834c..1cdb0fc5e 100644 --- a/databox/api/src/Api/Model/Output/ShareAlternateUrlOutput.php +++ b/databox/api/src/Api/Model/Output/ShareAlternateUrlOutput.php @@ -11,8 +11,7 @@ public function __construct( private string $name, private string $url, private ?string $type, - ) - { + ) { } #[Groups([Share::GROUP_READ])] diff --git a/databox/api/src/Api/OutputTransformer/CollectionOutputTransformer.php b/databox/api/src/Api/OutputTransformer/CollectionOutputTransformer.php index d4e658fe8..6e3c36029 100644 --- a/databox/api/src/Api/OutputTransformer/CollectionOutputTransformer.php +++ b/databox/api/src/Api/OutputTransformer/CollectionOutputTransformer.php @@ -21,7 +21,7 @@ class CollectionOutputTransformer implements OutputTransformerInterface use GroupsHelperTrait; use UserOutputTransformerTrait; use SecurityAwareTrait; - final public const COLLECTION_CACHE_NS = 'coll_visibility'; + final public const string COLLECTION_CACHE_NS = 'coll_visibility'; public function __construct( private readonly CollectionSearch $collectionSearch, @@ -71,9 +71,8 @@ public function transform($data, string $outputClass, array &$context = []): obj } $key = sprintf(AbstractObjectNormalizer::DEPTH_KEY_PATTERN, $output::class, 'children'); - $maxDepth = $this->hasGroup(Collection::GROUP_2LEVEL_CHILDREN, $context) ? 2 : 1; $depth = $context[$key] ?? 0; - if ($depth < $maxDepth) { + if ($depth < 1) { if (false !== $data->getHasChildren()) { $collections = $this->collectionSearch->search($context['userId'], $context['groupIds'], [ 'parent' => $data->getId(), diff --git a/databox/api/src/Api/Provider/ShareReadProvider.php b/databox/api/src/Api/Provider/ShareReadProvider.php index 78d1ace22..76949645b 100644 --- a/databox/api/src/Api/Provider/ShareReadProvider.php +++ b/databox/api/src/Api/Provider/ShareReadProvider.php @@ -55,10 +55,10 @@ public function provideShare(Share $item): Share $item->alternateUrls[] = new ShareAlternateUrlOutput( $definition->getName(), $this->urlGenerator->generate('share_public_rendition', [ - 'id' => $item->getId(), - 'rendition' => $definition->getId(), - 'token' => $item->getToken(), - ], UrlGeneratorInterface::ABS_URL), + 'id' => $item->getId(), + 'rendition' => $definition->getId(), + 'token' => $item->getToken(), + ], UrlGeneratorInterface::ABS_URL), $rendition->getFile()->getType(), ); } diff --git a/databox/api/src/Api/Provider/ShareRenditionProvider.php b/databox/api/src/Api/Provider/ShareRenditionProvider.php index ee2401bad..f56c1447d 100644 --- a/databox/api/src/Api/Provider/ShareRenditionProvider.php +++ b/databox/api/src/Api/Provider/ShareRenditionProvider.php @@ -52,7 +52,6 @@ public function provide(Operation $operation, array $uriVariables = [], array $c 'createdAt' => 'DESC', ]); - if (null !== $file = $rendition?->getFile()) { return new RedirectResponse($this->fileUrlResolver->resolveUrl($file)); } diff --git a/databox/api/src/Attribute/Type/DateTimeAttributeType.php b/databox/api/src/Attribute/Type/DateTimeAttributeType.php index ee6655f35..447d7b5d5 100644 --- a/databox/api/src/Attribute/Type/DateTimeAttributeType.php +++ b/databox/api/src/Attribute/Type/DateTimeAttributeType.php @@ -43,16 +43,20 @@ public function getGroupValueLabel($value): ?string public function createFilterQuery(string $field, $value): AbstractQuery { - $startFloor = (new \DateTimeImmutable()) - ->setTimestamp((int) $value[0]); + $criteria = []; + if (null !== $value[0]) { + $criteria['gte'] = (new \DateTimeImmutable()) + ->setTimestamp((int) $value[0]) + ->getTimestamp() * 1000; + } - $endCeil = (new \DateTimeImmutable()) - ->setTimestamp((int) $value[1]); + if (null !== $value[1]) { + $criteria['lte'] = (new \DateTimeImmutable()) + ->setTimestamp((int) $value[1]) + ->getTimestamp() * 1000; + } - return new Range($field, [ - 'gte' => $startFloor->getTimestamp() * 1000, - 'lte' => $endCeil->getTimestamp() * 1000, - ]); + return new Range($field, $criteria); } public function getFacetType(): string diff --git a/databox/api/src/Controller/Admin/AssetCrudController.php b/databox/api/src/Controller/Admin/AssetCrudController.php index e724c61ca..2dc879cbc 100644 --- a/databox/api/src/Controller/Admin/AssetCrudController.php +++ b/databox/api/src/Controller/Admin/AssetCrudController.php @@ -68,7 +68,7 @@ public function triggerIngest(AdminContext $context): Response WorkflowState::INITIATOR_ID => $user->getId(), ]); - return $this->redirect($context->getReferrer()); + return $this->returnToReferer($context); } public function configureCrud(Crud $crud): Crud diff --git a/databox/api/src/Controller/Admin/AssetDataTemplateCrudController.php b/databox/api/src/Controller/Admin/AssetDataTemplateCrudController.php index 426faeb7b..b89aa1d4a 100644 --- a/databox/api/src/Controller/Admin/AssetDataTemplateCrudController.php +++ b/databox/api/src/Controller/Admin/AssetDataTemplateCrudController.php @@ -12,7 +12,6 @@ use EasyCorp\Bundle\EasyAdminBundle\Config\Filters; use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField; use EasyCorp\Bundle\EasyAdminBundle\Field\BooleanField; -use EasyCorp\Bundle\EasyAdminBundle\Field\CollectionField; use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField; use EasyCorp\Bundle\EasyAdminBundle\Field\TextField; use EasyCorp\Bundle\EasyAdminBundle\Filter\DateTimeFilter; diff --git a/databox/api/src/Controller/Admin/JobStateCrudController.php b/databox/api/src/Controller/Admin/JobStateCrudController.php index 9ebc921c3..475e5d6a2 100644 --- a/databox/api/src/Controller/Admin/JobStateCrudController.php +++ b/databox/api/src/Controller/Admin/JobStateCrudController.php @@ -21,6 +21,7 @@ use EasyCorp\Bundle\EasyAdminBundle\Field\TextField; use EasyCorp\Bundle\EasyAdminBundle\Filter\ChoiceFilter; use EasyCorp\Bundle\EasyAdminBundle\Filter\DateTimeFilter; +use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator; use Symfony\Component\HttpFoundation\RedirectResponse; class JobStateCrudController extends AbstractAdminCrudController @@ -63,7 +64,7 @@ public function retryJob(AdminContext $context): RedirectResponse $jobState = $context->getEntity()->getInstance(); $this->workflowOrchestrator->retryFailedJobs($jobState->getWorkflow()->getId(), $jobState->getJobState()->getJobId()); - return new RedirectResponse($context->getReferrer()); + return $this->returnToReferer($context); } public function cancelJob(AdminContext $context): RedirectResponse @@ -72,7 +73,7 @@ public function cancelJob(AdminContext $context): RedirectResponse $jobState = $context->getEntity()->getInstance(); $this->workflowOrchestrator->cancelWorkflow($jobState->getWorkflow()->getId()); - return new RedirectResponse($context->getReferrer()); + return $this->returnToReferer($context); } public function rerunJob(AdminContext $context): RedirectResponse @@ -81,7 +82,7 @@ public function rerunJob(AdminContext $context): RedirectResponse $jobState = $context->getEntity()->getInstance(); $this->workflowOrchestrator->rerunJobs($jobState->getWorkflow()->getId(), $jobState->getJobState()->getJobId()); - return new RedirectResponse($context->getReferrer()); + return $this->returnToReferer($context); } public function configureCrud(Crud $crud): Crud diff --git a/databox/api/src/Controller/Admin/WorkflowStateCrudController.php b/databox/api/src/Controller/Admin/WorkflowStateCrudController.php index 2faa9fef5..39ad16a85 100644 --- a/databox/api/src/Controller/Admin/WorkflowStateCrudController.php +++ b/databox/api/src/Controller/Admin/WorkflowStateCrudController.php @@ -40,7 +40,7 @@ public function cancelWorkflow(AdminContext $context): RedirectResponse $workflowState = $context->getEntity()->getInstance(); $this->workflowOrchestrator->cancelWorkflow($workflowState->getId()); - return new RedirectResponse($context->getReferrer()); + return $this->returnToReferer($context); } public function configureActions(Actions $actions): Actions diff --git a/databox/api/src/Elasticsearch/Facet/CollectionFacet.php b/databox/api/src/Elasticsearch/Facet/CollectionFacet.php index 78a86dfee..a7f74bdb4 100644 --- a/databox/api/src/Elasticsearch/Facet/CollectionFacet.php +++ b/databox/api/src/Elasticsearch/Facet/CollectionFacet.php @@ -104,6 +104,10 @@ private function normalizeCollectionPath(string $path): ?string $pColl = $pColl->getParent(); } + if (empty($levels)) { + return null; + } + return implode(' / ', array_reverse($levels)); } } diff --git a/databox/api/src/Elasticsearch/Listener/CollectionPostTransformListener.php b/databox/api/src/Elasticsearch/Listener/CollectionPostTransformListener.php index b75d5c145..b9d9a1047 100644 --- a/databox/api/src/Elasticsearch/Listener/CollectionPostTransformListener.php +++ b/databox/api/src/Elasticsearch/Listener/CollectionPostTransformListener.php @@ -26,7 +26,7 @@ public function hydrateDocument(PostTransformEvent $event): void $document = $event->getDocument(); - $bestPrivacy = $collection->getBestPrivacyInParentHierarchy(); + $bestPrivacy = $collection->getPrivacy(); $users = $this->permissionManager->getAllowedUsers($collection, PermissionInterface::VIEW); $groups = $this->permissionManager->getAllowedGroups($collection, PermissionInterface::VIEW); @@ -35,21 +35,35 @@ public function hydrateDocument(PostTransformEvent $event): void $nlUsers = $users; $nlGroups = $groups; - if (!in_array(null, $users, true)) { - $parent = $collection->getParent(); - while (null !== $parent) { - $users = array_merge($users, $this->permissionManager->getAllowedUsers($parent, PermissionInterface::VIEW)); - if (in_array(null, $users, true)) { - break; - } + $parent = $collection->getParent(); + while (null !== $parent) { + $bestPrivacy = max($bestPrivacy, $parent->getPrivacy()); + if ($bestPrivacy >= WorkspaceItemPrivacyInterface::PUBLIC_FOR_USERS) { + $nlUsers = []; + $nlGroups = []; + break; + } + + $parentUsers = $this->permissionManager->getAllowedUsers($parent, PermissionInterface::VIEW); + $users = array_merge($users, $parentUsers); + $nlUsers = array_diff($nlUsers, $parentUsers); - $groups = array_merge($groups, $this->permissionManager->getAllowedGroups($parent, PermissionInterface::VIEW)); - $parent = $parent->getParent(); + if (in_array(null, $users, true)) { + $nlUsers = []; + $nlGroups = []; + $bestPrivacy = max($bestPrivacy, WorkspaceItemPrivacyInterface::PUBLIC_FOR_USERS); + break; } + + $parentGroups = $this->permissionManager->getAllowedGroups($parent, PermissionInterface::VIEW); + $groups = array_merge($groups, $parentGroups); + $nlGroups = array_diff($nlGroups, $parentGroups); + + $parent = $parent->getParent(); } if (in_array(null, $users, true)) { - $users = ['*']; + $users = []; $groups = []; $bestPrivacy = max($bestPrivacy, WorkspaceItemPrivacyInterface::PUBLIC_FOR_USERS); } diff --git a/databox/api/src/Entity/Core/Collection.php b/databox/api/src/Entity/Core/Collection.php index b75de96b4..660381e50 100644 --- a/databox/api/src/Entity/Core/Collection.php +++ b/databox/api/src/Entity/Core/Collection.php @@ -46,7 +46,10 @@ operations: [ new Get( normalizationContext: [ - 'groups' => [self::GROUP_READ], + 'groups' => [ + self::GROUP_READ, + self::GROUP_ABSOLUTE_TITLE, + ], ], security: 'is_granted("'.AbstractVoter::LIST.'", object)' ), @@ -82,7 +85,6 @@ 'groups' => [ self::GROUP_LIST, self::GROUP_CHILDREN, - self::GROUP_2LEVEL_CHILDREN, ], ], input: CollectionInput::class, @@ -103,11 +105,10 @@ class Collection extends AbstractUuidEntity implements SoftDeleteableInterface, use LocaleTrait; use WorkspacePrivacyTrait; - final public const GROUP_READ = 'coll:read'; - final public const GROUP_LIST = 'coll:index'; - final public const GROUP_CHILDREN = 'coll:ic'; - final public const GROUP_2LEVEL_CHILDREN = 'coll:2lc'; - final public const GROUP_ABSOLUTE_TITLE = 'coll:absTitle'; + final public const string GROUP_READ = 'coll:read'; + final public const string GROUP_LIST = 'coll:index'; + final public const string GROUP_CHILDREN = 'coll:ic'; + final public const string GROUP_ABSOLUTE_TITLE = 'coll:absTitle'; #[ORM\Column(type: Types::STRING, length: 255, nullable: true)] private ?string $title = null; @@ -241,7 +242,7 @@ private function computePrivacyRoots(): array { $roots = []; for ($i = WorkspaceItemPrivacyInterface::PRIVATE_IN_WORKSPACE; $i <= WorkspaceItemPrivacyInterface::PUBLIC; ++$i) { - $roots[$i] = $this->privacy === $i; + $roots[$i] = $this->privacy >= $i; } if (null !== $this->parent) { diff --git a/databox/api/src/Security/Voter/AbstractVoter.php b/databox/api/src/Security/Voter/AbstractVoter.php index ab35eb492..48e376d45 100644 --- a/databox/api/src/Security/Voter/AbstractVoter.php +++ b/databox/api/src/Security/Voter/AbstractVoter.php @@ -22,6 +22,8 @@ abstract class AbstractVoter extends Voter final public const EDIT = 'EDIT'; final public const DELETE = 'DELETE'; final public const EDIT_PERMISSIONS = 'EDIT_PERMISSIONS'; + final public const OPERATOR = 'OPERATOR'; + final public const OWNER = 'OWNER'; protected EntityManagerInterface $em; protected Security $security; diff --git a/databox/api/src/Security/Voter/AssetVoter.php b/databox/api/src/Security/Voter/AssetVoter.php index ea0fa21bf..8188c9ec4 100644 --- a/databox/api/src/Security/Voter/AssetVoter.php +++ b/databox/api/src/Security/Voter/AssetVoter.php @@ -57,17 +57,17 @@ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $ return $isOwner() || $this->security->isGranted(self::SCOPE_PREFIX.'EDIT') || $this->hasAcl(PermissionInterface::OPERATOR, $subject, $token) - || $this->containerHasAcl($subject, PermissionInterface::OPERATOR, $token); + || $this->voteOnContainer($subject, AbstractVoter::OPERATOR); case self::EDIT_ATTRIBUTES: return $isOwner() || $this->security->isGranted(self::SCOPE_PREFIX.'EDIT') || $this->hasAcl(PermissionInterface::EDIT, $subject, $token) - || $this->containerHasAcl($subject, PermissionInterface::EDIT, $token); + || $this->voteOnContainer($subject, AbstractVoter::EDIT); case self::SHARE: return $isOwner() || $this->security->isGranted(self::SCOPE_PREFIX.'EDIT') || $this->hasAcl(PermissionInterface::SHARE, $subject, $token) - || $this->containerHasAcl($subject, PermissionInterface::EDIT, $token); + || $this->voteOnContainer($subject, AbstractVoter::EDIT); case self::DELETE: return $isOwner() || $this->security->isGranted(self::SCOPE_PREFIX.'DELETE') @@ -80,19 +80,15 @@ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $ return $isOwner() || $this->security->isGranted(self::SCOPE_PREFIX.'OWNER') || $this->hasAcl(PermissionInterface::OWNER, $subject, $token) - || $this->containerHasAcl($subject, PermissionInterface::OWNER, $token); + || $this->voteOnContainer($subject, AbstractVoter::OWNER); } return false; } - private function containerHasAcl(Asset $asset, int $permission, TokenInterface $token): bool + private function voteOnContainer(Asset $asset, string|int $attribute): bool { - if (null !== $collection = $asset->getReferenceCollection()) { - return $this->hasAcl($permission, $collection, $token); - } - - return $this->hasAcl($permission, $asset->getWorkspace(), $token); + return $this->security->isGranted($attribute, $asset->getReferenceCollection() ?? $asset->getWorkspace()); } private function collectionGrantsAccess(Asset $subject): bool diff --git a/databox/api/src/Security/Voter/CollectionVoter.php b/databox/api/src/Security/Voter/CollectionVoter.php index e47f706aa..3da71862f 100644 --- a/databox/api/src/Security/Voter/CollectionVoter.php +++ b/databox/api/src/Security/Voter/CollectionVoter.php @@ -66,6 +66,12 @@ private function doVote(string $attribute, Collection $subject, TokenInterface $ self::EDIT_PERMISSIONS => $isOwner() || $this->hasAcl(PermissionInterface::OWNER, $subject, $token) || (null !== $subject->getParent() && $this->security->isGranted($attribute, $subject->getParent())), + self::OPERATOR => $isOwner() + || $this->hasAcl(PermissionInterface::OPERATOR, $subject, $token) + || (null !== $subject->getParent() && $this->security->isGranted($attribute, $subject->getParent())), + self::OWNER => $isOwner() + || $this->hasAcl(PermissionInterface::OWNER, $subject, $token) + || (null !== $subject->getParent() && $this->security->isGranted($attribute, $subject->getParent())), default => false, }; } diff --git a/databox/api/src/Security/Voter/WorkspaceVoter.php b/databox/api/src/Security/Voter/WorkspaceVoter.php index d10c27284..6ebdc1850 100644 --- a/databox/api/src/Security/Voter/WorkspaceVoter.php +++ b/databox/api/src/Security/Voter/WorkspaceVoter.php @@ -64,6 +64,12 @@ private function doVote(string $attribute, Workspace $subject, TokenInterface $t self::EDIT_PERMISSIONS => $isOwner() || $this->hasAcl(PermissionInterface::OWNER, $subject, $token) || $this->isAdmin(), + self::OPERATOR => $isOwner() + || $this->hasAcl(PermissionInterface::OPERATOR, $subject, $token), + self::OWNER => $isOwner() + || $this->hasAcl(PermissionInterface::OWNER, $subject, $token) + || $this->isAdmin(), + default => false, }; } diff --git a/databox/api/tests/DataboxTestTrait.php b/databox/api/tests/DataboxTestTrait.php index 4b0839e21..cb82bbd25 100644 --- a/databox/api/tests/DataboxTestTrait.php +++ b/databox/api/tests/DataboxTestTrait.php @@ -106,6 +106,9 @@ protected function createCollection(array $options = []): Collection if ($options['public'] ?? false) { $collection->setPrivacy(WorkspaceItemPrivacyInterface::PUBLIC); } + if ($options['parent'] ?? false) { + $collection->setParent($options['parent']); + } $em->persist($collection); if (!($options['no_flush'] ?? false)) { diff --git a/databox/api/tests/CollectionSearchTest.php b/databox/api/tests/Search/CollectionSearchTest.php similarity index 62% rename from databox/api/tests/CollectionSearchTest.php rename to databox/api/tests/Search/CollectionSearchTest.php index 9caa1ad45..c5831267e 100644 --- a/databox/api/tests/CollectionSearchTest.php +++ b/databox/api/tests/Search/CollectionSearchTest.php @@ -2,12 +2,11 @@ declare(strict_types=1); -namespace App\Tests; +namespace App\Tests\Search; use Alchemy\AclBundle\Model\AccessControlEntryInterface; use Alchemy\AclBundle\Security\PermissionInterface; use Alchemy\AuthBundle\Tests\Client\KeycloakClientTestMock; -use App\Tests\Search\AbstractSearchTest; class CollectionSearchTest extends AbstractSearchTest { @@ -17,6 +16,114 @@ private static function releaseIndex(): void self::waitForESIndex('collection'); } + public function testSearchRootWithCollectionsInNonPublicWorkspaceAsAnonymousUser(): void + { + $A = $this->createCollection([ + 'title' => 'A', + ]); + $this->createCollection([ + 'title' => 'B', + 'parent' => $A, + 'public' => true, + ]); + self::releaseIndex(); + + $response = $this->request( + null, + 'GET', + '/collections', + ); + + $data = $this->getDataFromResponse($response, 200); + $this->assertCount(0, $data); + } + + public function testSearchRootWithNonPublicCollectionsInPublicWorkspaceAsAnonymousUser(): void + { + $workspace = $this->createWorkspace([ + 'title' => 'Workspace', + 'public' => true, + ]); + $A = $this->createCollection([ + 'title' => 'A', + 'workspace' => $workspace, + ]); + $this->createCollection([ + 'title' => 'B', + 'parent' => $A, + 'workspace' => $workspace, + ]); + self::releaseIndex(); + + $response = $this->request( + null, + 'GET', + '/collections', + ); + + $data = $this->getDataFromResponse($response, 200); + $this->assertCount(0, $data); + } + + public function testSearchRootWithOnePublicSubCollectionInPublicWorkspaceAsAnonymousUser(): void + { + $workspace = $this->createWorkspace([ + 'title' => 'Workspace', + 'public' => true, + ]); + $A = $this->createCollection([ + 'title' => 'A', + 'workspace' => $workspace, + ]); + $this->createCollection([ + 'title' => 'B', + 'parent' => $A, + 'public' => true, + 'workspace' => $workspace, + ]); + self::releaseIndex(); + + $response = $this->request( + null, + 'GET', + '/collections', + ); + + $data = $this->getDataFromResponse($response, 200); + $this->assertCount(1, $data); + $this->assertSame('B', $data[0]['title']); + } + + public function testSearchRootWithTwoPublicSubCollectionsInPublicWorkspaceAsAnonymousUser(): void + { + $workspace = $this->createWorkspace([ + 'title' => 'Workspace', + 'public' => true, + ]); + $A = $this->createCollection([ + 'title' => 'A', + 'public' => true, + 'workspace' => $workspace, + ]); + $this->createCollection([ + 'title' => 'B', + 'parent' => $A, + 'public' => true, + 'workspace' => $workspace, + ]); + self::releaseIndex(); + + $response = $this->request( + null, + 'GET', + '/collections', + ); + + $data = $this->getDataFromResponse($response, 200); + $this->assertCount(1, $data); + $this->assertSame('A', $data[0]['title']); + } + public function testSearchPublicCollectionsInPrivateWorkspaceAsAnonymousUser(): void { $this->createCollection([ diff --git a/databox/client/src/api/collection.ts b/databox/client/src/api/collection.ts index 6b5540402..abdd39edc 100644 --- a/databox/client/src/api/collection.ts +++ b/databox/client/src/api/collection.ts @@ -35,32 +35,6 @@ export function clearWorkspaceCache(): void { delete cache.ws; } -export async function getWorkspaces(): Promise { - // eslint-disable-next-line no-prototype-builtins - if (cache.hasOwnProperty('ws')) { - return cache.ws; - } - - const collections = await getCollections({ - groupByWorkspace: true, - limit: collectionChildrenLimit, - }); - - const workspaces: {[key: string]: Workspace} = {}; - - collections.result.forEach((c: Collection) => { - if (!workspaces[c.workspace.id]) { - workspaces[c.workspace.id] = { - ...c.workspace, - collections: [], - }; - } - workspaces[c.workspace.id].collections.push(c); - }); - - return (cache.ws = Object.keys(workspaces).map(i => workspaces[i])); -} - export async function getCollection(id: string): Promise { return (await apiClient.get(`/collections/${id}`)).data; } diff --git a/databox/client/src/api/workspace.ts b/databox/client/src/api/workspace.ts index f3f837c84..0ddf47d41 100644 --- a/databox/client/src/api/workspace.ts +++ b/databox/client/src/api/workspace.ts @@ -1,8 +1,17 @@ import apiClient from './api-client'; import {Workspace} from '../types'; +import {ApiCollectionResponse, getHydraCollection} from './hydra.ts'; export async function getWorkspace(id: string): Promise { const res = await apiClient.get(`/workspaces/${id}`); return res.data; } + +export async function getWorkspaces(): Promise< + ApiCollectionResponse +> { + const res = await apiClient.get('/workspaces'); + + return getHydraCollection(res.data); +} diff --git a/databox/client/src/components/Acl/AclForm.tsx b/databox/client/src/components/Acl/AclForm.tsx index c2824f572..223d1cfdb 100644 --- a/databox/client/src/components/Acl/AclForm.tsx +++ b/databox/client/src/components/Acl/AclForm.tsx @@ -2,8 +2,7 @@ import {useCallback} from 'react'; import PermissionList from '../Permissions/PermissionList'; import {deleteAce, getAces, putAce} from '../../api/acl'; import {OnPermissionDelete, PermissionObject} from '../Permissions/permissions'; -import {Ace, UserType} from '../../types'; -import {useCollectionStore} from '../../store/collectionStore'; +import {UserType} from '../../types'; type Props = { objectType: PermissionObject; @@ -34,20 +33,8 @@ export default function AclForm({ [objectType, objectId] ); - const onListChanged = - objectType === 'collection' - ? (permissions: Ace[]) => { - useCollectionStore - .getState() - .partialUpdateCollection(objectId, { - shared: permissions.length > 0, - }); - } - : undefined; - return ( (); - React.useEffect(() => { + useEffectOnce(() => { load(); }, []); diff --git a/databox/client/src/components/Dialog/Asset/OperationsAsset.tsx b/databox/client/src/components/Dialog/Asset/OperationsAsset.tsx index e22cb539a..573a561ad 100644 --- a/databox/client/src/components/Dialog/Asset/OperationsAsset.tsx +++ b/databox/client/src/components/Dialog/Asset/OperationsAsset.tsx @@ -64,7 +64,8 @@ export default function OperationsAsset({ } /> + } + label={t('collection.info.workspace', `Workspace`)} + value={data.workspace.name} + copyValue={data.workspace.id} + /> + } + label={t('collection.info.absolute_path', `Absolute Path`)} + value={data.absoluteTitle} + /> ); diff --git a/databox/client/src/components/Dialog/Tabbed/FormTab.tsx b/databox/client/src/components/Dialog/Tabbed/FormTab.tsx index 1f78244fe..1e37e9e27 100644 --- a/databox/client/src/components/Dialog/Tabbed/FormTab.tsx +++ b/databox/client/src/components/Dialog/Tabbed/FormTab.tsx @@ -6,9 +6,7 @@ import {LoadingButton} from '@mui/lab'; import SaveIcon from '@mui/icons-material/Save'; import RemoteErrors from '../../Form/RemoteErrors'; import {useTranslation} from 'react-i18next'; -import { - useFormPrompt, -} from '@alchemy/navigation'; +import {useFormPrompt} from '@alchemy/navigation'; type Props = PropsWithChildren<{ loading: boolean; diff --git a/databox/client/src/components/Form/CollectionTreeWidget.tsx b/databox/client/src/components/Form/CollectionTreeWidget.tsx index fb7ccf143..7637c6d79 100644 --- a/databox/client/src/components/Form/CollectionTreeWidget.tsx +++ b/databox/client/src/components/Form/CollectionTreeWidget.tsx @@ -6,10 +6,10 @@ import {FieldPath} from 'react-hook-form'; import { CollectionsTreeView, CollectionTreeViewProps, - IsSelectable, -} from '../Media/Collection/CollectionsTreeView'; +} from '../Media/Collection/CollectionTree/CollectionsTreeView.tsx'; import {FormControl, FormLabel} from '@mui/material'; import {RegisterOptions} from 'react-hook-form'; +import {IsSelectable} from '../Media/Collection/CollectionTree/collectionTree.ts'; type Props = { label?: ReactNode; diff --git a/databox/client/src/components/Integration/Phrasea/Expose/CreatePublicationDialog.tsx b/databox/client/src/components/Integration/Phrasea/Expose/CreatePublicationDialog.tsx index f2ed88b55..0ed989544 100644 --- a/databox/client/src/components/Integration/Phrasea/Expose/CreatePublicationDialog.tsx +++ b/databox/client/src/components/Integration/Phrasea/Expose/CreatePublicationDialog.tsx @@ -5,11 +5,7 @@ import RemoteErrors from '../../../Form/RemoteErrors'; import {FormFieldErrors, FormRow} from '@alchemy/react-form'; import {useTranslation} from 'react-i18next'; import {useFormSubmit} from '@alchemy/api'; -import { - StackedModalProps, - useModals, - useFormPrompt, -} from '@alchemy/navigation'; +import {StackedModalProps, useModals, useFormPrompt} from '@alchemy/navigation'; import {Basket, IntegrationData} from '../../../../types.ts'; import {runIntegrationAction} from '../../../../api/integrations.ts'; import {SwitchWidget} from '@alchemy/react-form'; diff --git a/databox/client/src/components/Media/Asset/Actions/AssetViewActions.tsx b/databox/client/src/components/Media/Asset/Actions/AssetViewActions.tsx index 7facd3936..6deb2b2a6 100644 --- a/databox/client/src/components/Media/Asset/Actions/AssetViewActions.tsx +++ b/databox/client/src/components/Media/Asset/Actions/AssetViewActions.tsx @@ -19,13 +19,22 @@ type Props = { export default function AssetViewActions({asset, file}: Props) { const {t} = useTranslation(); const closeModal = useCloseModal(); - const {onDelete, onDownload, onEdit, onEditAttr, onShare, onSubstituteFile, can} = - useAssetActions({asset, onDelete: closeModal}); + const { + onDelete, + onDownload, + onEdit, + onEditAttr, + onShare, + onSubstituteFile, + can, + } = useAssetActions({asset, onDelete: closeModal}); return ( <> * + *': { @@ -89,9 +98,11 @@ export default function AssetViewActions({asset, file}: Props) { '' )} {can.share ? ( - ) : ( diff --git a/databox/client/src/components/Media/Asset/Actions/SaveFileAsRenditionDialog.tsx b/databox/client/src/components/Media/Asset/Actions/SaveFileAsRenditionDialog.tsx index 5da59569b..4086bf7ad 100644 --- a/databox/client/src/components/Media/Asset/Actions/SaveFileAsRenditionDialog.tsx +++ b/databox/client/src/components/Media/Asset/Actions/SaveFileAsRenditionDialog.tsx @@ -10,11 +10,7 @@ import RenditionDefinitionSelect from '../../../Form/RenditionDefinitionSelect'; import {useTranslation} from 'react-i18next'; import {postRendition} from '../../../../api/rendition'; import {useFormSubmit} from '@alchemy/api'; -import { - useModals, - StackedModalProps, - useFormPrompt, -} from '@alchemy/navigation'; +import {useModals, StackedModalProps, useFormPrompt} from '@alchemy/navigation'; type FormData = { definition: string | undefined; diff --git a/databox/client/src/components/Media/Asset/Facets.tsx b/databox/client/src/components/Media/Asset/Facets.tsx index 5f85d0ce3..9fbcb9c64 100644 --- a/databox/client/src/components/Media/Asset/Facets.tsx +++ b/databox/client/src/components/Media/Asset/Facets.tsx @@ -68,7 +68,7 @@ export function extractLabelValueFromKey( format?: AttributeFormat ): LabelledBucketValue { // eslint-disable-next-line no-prototype-builtins - if (typeof key === 'object' && key.hasOwnProperty('value')) { + if (key && typeof key === 'object' && key.hasOwnProperty('value')) { return key as LabelledBucketValue; } diff --git a/databox/client/src/components/Media/Collection/CollectionMoveSection.tsx b/databox/client/src/components/Media/Collection/CollectionMoveSection.tsx index 732715a62..fe9c4647c 100644 --- a/databox/client/src/components/Media/Collection/CollectionMoveSection.tsx +++ b/databox/client/src/components/Media/Collection/CollectionMoveSection.tsx @@ -2,14 +2,12 @@ import {useState} from 'react'; import {Collection} from '../../../types'; import {useTranslation} from 'react-i18next'; import {Typography} from '@mui/material'; -import { - CollectionsTreeView, - treeViewPathSeparator, -} from './CollectionsTreeView'; +import {CollectionsTreeView} from './CollectionTree/CollectionsTreeView.tsx'; import {clearWorkspaceCache, moveCollection} from '../../../api/collection'; import {toast} from 'react-toastify'; import {LoadingButton} from '@mui/lab'; import DriveFileMoveIcon from '@mui/icons-material/DriveFileMove'; +import {treeViewPathSeparator} from './CollectionTree/collectionTree.ts'; type Props = { collection: Collection; @@ -70,7 +68,7 @@ export default function CollectionMoveSection({collection, onMoved}: Props) { disabled={loading || !dest} loading={loading} > - {t('', 'Move collection')} + {t('collection_move.move.label', 'Move collection')} ); diff --git a/databox/client/src/components/Media/Collection/CollectionTree/CollectionTreeItem.tsx b/databox/client/src/components/Media/Collection/CollectionTree/CollectionTreeItem.tsx new file mode 100644 index 000000000..08903416a --- /dev/null +++ b/databox/client/src/components/Media/Collection/CollectionTree/CollectionTreeItem.tsx @@ -0,0 +1,143 @@ +import React, {useCallback} from 'react'; +import { + CollectionPager, + useCollectionStore, +} from '../../../../store/collectionStore.ts'; +import EditableCollectionTree, { + defaultNewCollectionName, +} from '../EditableTree.tsx'; +import {TreeItem} from '@mui/x-tree-view'; +import {IconButton, Stack} from '@mui/material'; +import CreateNewFolderIcon from '@mui/icons-material/CreateNewFolder'; +import {CollectionOptionalWorkspace} from '../../../../types.ts'; +import {CommonTreeItemProps, treeViewPathSeparator} from './collectionTree.ts'; +import TreeItemLoader from './TreeItemLoader.tsx'; + +type Props = { + collection: CollectionOptionalWorkspace; + depth?: number; + workspaceId: string; +} & CommonTreeItemProps; + +export function CollectionTreeItem({ + updateCollectionPath, + newCollectionPath, + setNewCollectionPath, + collection, + workspaceId, + disabledBranches, + setExpanded, + allowNew, + isSelectable, + depth = 0, +}: Props) { + const [loaded, setLoaded] = React.useState(false); + const loadCollections = useCollectionStore(state => state.load); + + const pager = + useCollectionStore(state => state.tree)[collection.id] ?? + ({ + items: collection.children, + expanding: false, + loadingMore: false, + } as CollectionPager); + + async function load() { + if (!collection.children || collection.children.length === 0) { + return; + } + + if (!loaded) { + setLoaded(true); + await loadCollections(workspaceId, collection.id); + } + } + + const collectionIRI = collection['@id']; + const nodeId = workspaceId + treeViewPathSeparator + collectionIRI; + const hasTree = pager.items && pager.items.length > 0; + const hasNewCollectionPath = + newCollectionPath && newCollectionPath.rootNode === nodeId; + + const onCreateNewCollection = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + setNewCollectionPath( + [ + { + value: defaultNewCollectionName, + id: '0', + editing: true, + }, + ], + nodeId + ); + setExpanded(prev => + !prev.includes(nodeId) ? prev.concat(nodeId) : prev + ); + }, + [setNewCollectionPath, setExpanded, nodeId] + ); + + return ( + nodeId.startsWith(b))) || + (isSelectable && !isSelectable(collection)) + } + onClick={load} + nodeId={nodeId} + label={ + + {collection.title} + {allowNew && collection.capabilities.canEdit && ( + + + + )} + + } + > + {/*Wrapping all to avoid collapse in node */} + {pager.expanding || + hasTree || + (allowNew && hasNewCollectionPath) ? ( + <> + {pager.expanding ? : null} + {allowNew && hasNewCollectionPath ? ( + + ) : null} + {hasTree && + pager.items!.map(c => ( + + ))} + + ) : null} + + ); +} diff --git a/databox/client/src/components/Media/Collection/CollectionTree/CollectionsTreeView.tsx b/databox/client/src/components/Media/Collection/CollectionTree/CollectionsTreeView.tsx new file mode 100644 index 000000000..f09a86d68 --- /dev/null +++ b/databox/client/src/components/Media/Collection/CollectionTree/CollectionsTreeView.tsx @@ -0,0 +1,185 @@ +import React, {useCallback, useState} from 'react'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ChevronRightIcon from '@mui/icons-material/ChevronRight'; +import {TreeView} from '@mui/x-tree-view'; +import {CircularProgress} from '@mui/material'; +import useEffectOnce from '@alchemy/react-hooks/src/useEffectOnce'; +import {useWorkspaceStore} from '../../../../store/workspaceStore.ts'; +import { + Collection, + CommonTreeProps, + NewCollectionPathState, + normalizeNodeId, + SetNewCollectionPath, + treeViewPathSeparator, + UpdateCollectionPath, +} from './collectionTree.ts'; +import WorkspaceTreeItem from './WorkspaceTreeItem.tsx'; + +type Props = { + value?: IsMulti extends true ? Collection[] : Collection; + onChange?: ( + selection: IsMulti extends true ? string[] : string, + workspaceId?: IsMulti extends true ? string : never + ) => void; + workspaceId?: string; +} & CommonTreeProps; + +export type {Props as CollectionTreeViewProps}; + +export function CollectionsTreeView({ + onChange, + value, + multiple, + workspaceId, + disabledBranches, + allowNew, + disabled, + isSelectable, +}: Props) { + const loadWorkspaces = useWorkspaceStore(state => state.load); + const loading = useWorkspaceStore(state => state.loading); + const allWorkspaces = useWorkspaceStore(state => state.workspaces); + const workspaces = workspaceId + ? allWorkspaces.filter(w => w.id === workspaceId) + : allWorkspaces; + + useEffectOnce(() => { + loadWorkspaces(); + }, []); + + const [newCollectionPath, setNewCollectionPath] = + useState(); + const [expanded, setExpanded] = React.useState([]); + const [selected, setSelected] = React.useState< + IsMulti extends true ? string[] : string | undefined + >(value ?? ((multiple ? [] : '') as any)); + + const setNewCollectionPathProxy = useCallback( + (nodes, rootId) => { + setNewCollectionPath(prev => ({ + nodes, + rootNode: rootId ? rootId : prev!.rootNode, + })); + }, + [setNewCollectionPath] + ); + + const handleSelect = ( + _event: React.ChangeEvent<{}>, + nodeIds: IsMulti extends true ? string[] : string + ) => { + if (disabled) { + return; + } + if (multiple) { + const striped = (nodeIds as string[]).map(i => + normalizeNodeId(i, newCollectionPath) + ); + setSelected(nodeIds as any); + onChange && onChange(striped as any); + } else { + const striped = normalizeNodeId( + nodeIds as string, + newCollectionPath + ); + const workspaceId = + typeof striped === 'object' + ? striped.rootId?.split(treeViewPathSeparator)[0] + : (nodeIds as string).split(treeViewPathSeparator)[0]; + + setSelected(nodeIds); + onChange && onChange(striped as any, workspaceId as any); + } + }; + + const updateCollectionPath = useCallback( + (index, id, value, editing) => { + setNewCollectionPath(prev => { + if (index === 0 && id === null) { + return undefined; + } + + if (index >= (prev!.nodes?.length ?? 0)) { + return { + ...prev!, + nodes: prev!.nodes.concat({ + id: id!, + value: value!, + editing: editing!, + }), + }; + } + + return { + ...prev!, + nodes: + id === null + ? prev!.nodes.slice(0, index) + : prev!.nodes.map((p, i) => + i === index + ? { + id: id!, + value: value!, + editing: editing!, + } + : p + ), + }; + }); + }, + [setNewCollectionPath] + ); + + const handleToggle = (_event: React.ChangeEvent<{}>, nodeIds: string[]) => { + setExpanded(nodeIds); + }; + + if (loading) { + return ; + } + + return ( + ({ + 'flexGrow': 1, + '.MuiTreeItem-content': { + borderRadius: theme.shape.borderRadius, + width: 'fit-content', + }, + '.MuiTreeItem-content.Mui-selected, .MuiTreeItem-content.Mui-selected.Mui-focused': + { + bgcolor: 'primary.main', + color: 'primary.contrastText', + fontWeight: 700, + }, + '.MuiButtonBase-root': { + color: 'inherit', + }, + })} + defaultCollapseIcon={} + defaultExpandIcon={} + expanded={expanded} + selected={selected as any} + onNodeToggle={handleToggle} + onNodeSelect={handleSelect as any} + multiSelect={multiple || false} + > + {workspaces.map(w => { + return ( + + ); + })} + + ); +} diff --git a/databox/client/src/components/Media/Collection/CollectionTree/TreeItemLoader.tsx b/databox/client/src/components/Media/Collection/CollectionTree/TreeItemLoader.tsx new file mode 100644 index 000000000..9cc2c64dd --- /dev/null +++ b/databox/client/src/components/Media/Collection/CollectionTree/TreeItemLoader.tsx @@ -0,0 +1,34 @@ +import {CircularProgress, Typography} from '@mui/material'; +import {TreeItem} from '@mui/x-tree-view'; +import React from 'react'; +import {useTranslation} from 'react-i18next'; + +type Props = {}; + +function TreeItemLoader({}: Props) { + const {t} = useTranslation(); + return ( + + + {t('common.loading', 'Loading…')} + + } + /> + ); +} + +export default React.memo(TreeItemLoader, () => true); diff --git a/databox/client/src/components/Media/Collection/CollectionTree/WorkspaceTreeItem.tsx b/databox/client/src/components/Media/Collection/CollectionTree/WorkspaceTreeItem.tsx new file mode 100644 index 000000000..c8f9bd2c3 --- /dev/null +++ b/databox/client/src/components/Media/Collection/CollectionTree/WorkspaceTreeItem.tsx @@ -0,0 +1,91 @@ +import {Box, Typography} from '@mui/material'; +import {CollectionTreeItem} from './CollectionTreeItem.tsx'; +import {TreeItem} from '@mui/x-tree-view'; +import React from 'react'; +import {Workspace} from '../../../../types.ts'; +import {CommonTreeItemProps, treeViewPathSeparator} from './collectionTree.ts'; +import { + CollectionPager, + useCollectionStore, +} from '../../../../store/collectionStore.ts'; +import TreeItemLoader from './TreeItemLoader.tsx'; + +type Props = { + workspace: Workspace; +} & CommonTreeItemProps; + +export default function WorkspaceTreeItem({ + workspace, + disabledBranches, + ...rest +}: Props) { + const workspaceId = workspace.id; + const nodeId = workspaceId + treeViewPathSeparator + workspace['@id']; + const [loaded, setLoaded] = React.useState(false); + const loadRoot = useCollectionStore(state => state.load); + + const pager = + useCollectionStore(state => state.tree)[workspaceId] ?? + ({ + items: [], + expanding: false, + loadingMore: false, + } as CollectionPager); + + async function load() { + if (!loaded) { + setLoaded(true); + await loadRoot(workspaceId); + } + } + + return ( + <> + + + + {workspace.name} + + + + + } + disabled={ + disabledBranches && + disabledBranches.some(b => nodeId.startsWith(b)) + } + > + {pager.expanding ? : null} + {pager.items.map(c => ( + + ))} + + + ); +} diff --git a/databox/client/src/components/Media/Collection/CollectionTree/collectionTree.ts b/databox/client/src/components/Media/Collection/CollectionTree/collectionTree.ts new file mode 100644 index 000000000..6bebe1284 --- /dev/null +++ b/databox/client/src/components/Media/Collection/CollectionTree/collectionTree.ts @@ -0,0 +1,78 @@ +import {CollectionOptionalWorkspace} from '../../../../types.ts'; +import {nodeNewPrefix} from '../EditableTree.tsx'; + +const nodeSeparator = '|'; + +export {nodeSeparator as treeViewPathSeparator}; + +export type SetExpanded = ( + nodeIds: string[] | ((prevNodeIds: string[]) => string[]) +) => void; + +export type UpdateCollectionPath = ( + index: number, + id: string | null, + value?: string | null, + editing?: boolean +) => void; + +export type NewCollectionPath = { + rootId: string; + path: string[]; +}; + +export type CommonTreeItemProps = { + isSelectable?: IsSelectable; + newCollectionPath: NewCollectionPathState | undefined; + setNewCollectionPath: SetNewCollectionPath; + updateCollectionPath: UpdateCollectionPath; + setExpanded: SetExpanded; +} & CommonTreeProps; + +export type CommonTreeProps = { + multiple?: IsMulti; + disabledBranches?: string[]; + allowNew?: boolean; + disabled?: boolean | undefined; + isSelectable?: IsSelectable; +}; + +export type CollectionId = string; + +export type Collection = CollectionId | NewCollectionPath; + +export type IsSelectable = (collection: CollectionOptionalWorkspace) => boolean; + +export function normalizeNodeId( + nodeId: string, + newCollectionPath: NewCollectionPathState | undefined +): Collection { + if (newCollectionPath && nodeId.startsWith(nodeNewPrefix)) { + const offset = parseInt(nodeId.substring(nodeNewPrefix.length)); + + return { + rootId: newCollectionPath.rootNode, + path: new Array(offset + 1) + .fill(true, 0, offset + 1) + .map((_, i) => newCollectionPath.nodes[i].value), + }; + } + + return nodeId.split(nodeSeparator)[1]; +} + +export type NewCollectionNodeState = { + id: string; + value: string; + editing?: boolean | undefined; +}; + +export type NewCollectionPathState = { + rootNode: string; + nodes: NewCollectionNodeState[]; +}; + +export type SetNewCollectionPath = ( + nodes: NewCollectionNodeState[], + rootId?: string +) => void; diff --git a/databox/client/src/components/Media/Collection/CollectionsTreeView.tsx b/databox/client/src/components/Media/Collection/CollectionsTreeView.tsx deleted file mode 100644 index f8d8cdfe2..000000000 --- a/databox/client/src/components/Media/Collection/CollectionsTreeView.tsx +++ /dev/null @@ -1,425 +0,0 @@ -import React, {useCallback, useEffect, useState} from 'react'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import {TreeItem, TreeView} from '@mui/x-tree-view'; -import {CollectionOptionalWorkspace, Workspace} from '../../../types'; -import {getWorkspaces} from '../../../api/collection'; -import { - CollectionPager, - useCollectionStore, -} from '../../../store/collectionStore'; -import { - Box, - CircularProgress, - IconButton, - Stack, - Typography, -} from '@mui/material'; -import EditableCollectionTree, { - defaultNewCollectionName, - nodeNewPrefix, -} from './EditableTree'; -import CreateNewFolderIcon from '@mui/icons-material/CreateNewFolder'; - -const nodeSeparator = '|'; - -export {nodeSeparator as treeViewPathSeparator}; - -export type SetExpanded = ( - nodeIds: string[] | ((prevNodeIds: string[]) => string[]) -) => void; - -export type UpdateCollectionPath = ( - index: number, - id: string | null, - value?: string | null, - editing?: boolean -) => void; - -export type NewCollectionPath = { - rootId: string; - path: string[]; -}; - -export type CollectionId = string; - -export type Collection = CollectionId | NewCollectionPath; - -export type IsSelectable = (collection: CollectionOptionalWorkspace) => boolean; - -type CollectionTreeProps = { - newCollectionPath: NewCollectionPathState | undefined; - collection: CollectionOptionalWorkspace; - workspaceId: string; - depth?: number; - disabledBranches?: string[]; - setNewCollectionPath: SetNewCollectionPath; - updateCollectionPath: UpdateCollectionPath; - setExpanded: SetExpanded; - allowNew: boolean | undefined; - isSelectable?: IsSelectable; -}; - -function CollectionTree({ - updateCollectionPath, - newCollectionPath, - setNewCollectionPath, - collection, - workspaceId, - disabledBranches, - setExpanded, - allowNew, - isSelectable, - depth = 0, -}: CollectionTreeProps) { - const [loaded, setLoaded] = React.useState(false); - const loadChildren = useCollectionStore(state => state.loadChildren); - - const pager = - useCollectionStore(state => state.tree)[collection.id] ?? - ({ - items: collection.children, - expanding: false, - loadingMore: false, - } as CollectionPager); - - async function load() { - if (!collection.children || collection.children.length === 0) { - return; - } - - if (!loaded) { - setLoaded(true); - await loadChildren(workspaceId, collection.id); - } - } - - const collectionIRI = collection['@id']; - const nodeId = workspaceId + nodeSeparator + collectionIRI; - const hasTree = pager.items && pager.items.length > 0; - const hasNewCollectionPath = - newCollectionPath && newCollectionPath.rootNode === nodeId; - - const onCreateNewCollection = useCallback( - (e: React.MouseEvent) => { - e.stopPropagation(); - setNewCollectionPath( - [ - { - value: defaultNewCollectionName, - id: '0', - editing: true, - }, - ], - nodeId - ); - setExpanded(prev => - !prev.includes(nodeId) ? prev.concat(nodeId) : prev - ); - }, - [setNewCollectionPath, setExpanded, nodeId] - ); - - return ( - nodeId.startsWith(b))) || - (isSelectable && !isSelectable(collection)) - } - onClick={load} - nodeId={nodeId} - label={ - - {collection.title} - {allowNew && collection.capabilities.canEdit && ( - - - - )} - - } - > - {/*Wrapping all to avoid collapse in node */} - {hasTree || (allowNew && hasNewCollectionPath) ? ( - <> - {allowNew && hasNewCollectionPath ? ( - - ) : null} - {hasTree && - pager.items!.map(c => ( - - ))} - - ) : null} - - ); -} - -function normalizeNodeId( - nodeId: string, - newCollectionPath: NewCollectionPathState | undefined -): Collection { - if (newCollectionPath && nodeId.startsWith(nodeNewPrefix)) { - const offset = parseInt(nodeId.substring(nodeNewPrefix.length)); - - return { - rootId: newCollectionPath.rootNode, - path: new Array(offset + 1) - .fill(true, 0, offset + 1) - .map((_, i) => newCollectionPath.nodes[i].value), - }; - } - - return nodeId.split(nodeSeparator)[1]; -} - -export type NewCollectionNodeState = { - id: string; - value: string; - editing?: boolean | undefined; -}; - -type NewCollectionPathState = { - rootNode: string; - nodes: NewCollectionNodeState[]; -}; - -type SetNewCollectionPath = ( - nodes: NewCollectionNodeState[], - rootId?: string -) => void; - -type Props = { - onChange?: ( - selection: IsMulti extends true ? string[] : string, - workspaceId?: IsMulti extends true ? string : never - ) => void; - value?: IsMulti extends true ? Collection[] : Collection; - multiple?: IsMulti; - workspaceId?: string; - disabledBranches?: string[]; - allowNew?: boolean; - disabled?: boolean | undefined; - isSelectable?: IsSelectable; -}; - -export type {Props as CollectionTreeViewProps}; - -export function CollectionsTreeView({ - onChange, - value, - multiple, - workspaceId, - disabledBranches, - allowNew, - disabled, - isSelectable, -}: Props) { - const [workspaces, setWorkspaces] = useState(); - - const [newCollectionPath, setNewCollectionPath] = - useState(); - const [expanded, setExpanded] = React.useState([]); - const [selected, setSelected] = React.useState< - IsMulti extends true ? string[] : string | undefined - >(value ?? ((multiple ? [] : '') as any)); - - const setNewCollectionPathProxy = useCallback( - (nodes, rootId) => { - setNewCollectionPath(prev => ({ - nodes, - rootNode: rootId ? rootId : prev!.rootNode, - })); - }, - [setNewCollectionPath] - ); - - const handleSelect = ( - _event: React.ChangeEvent<{}>, - nodeIds: IsMulti extends true ? string[] : string - ) => { - if (disabled) { - return; - } - if (multiple) { - const striped = (nodeIds as string[]).map(i => - normalizeNodeId(i, newCollectionPath) - ); - setSelected(nodeIds as any); - onChange && onChange(striped as any); - } else { - const striped = normalizeNodeId( - nodeIds as string, - newCollectionPath - ); - const workspaceId = - typeof striped === 'object' - ? striped.rootId?.split(nodeSeparator)[0] - : (nodeIds as string).split(nodeSeparator)[0]; - - setSelected(nodeIds); - onChange && onChange(striped as any, workspaceId as any); - } - }; - - const updateCollectionPath = useCallback( - (index, id, value, editing) => { - setNewCollectionPath(prev => { - if (index === 0 && id === null) { - return undefined; - } - - if (index >= (prev!.nodes?.length ?? 0)) { - return { - ...prev!, - nodes: prev!.nodes.concat({ - id: id!, - value: value!, - editing: editing!, - }), - }; - } - - return { - ...prev!, - nodes: - id === null - ? prev!.nodes.slice(0, index) - : prev!.nodes.map((p, i) => - i === index - ? { - id: id!, - value: value!, - editing: editing!, - } - : p - ), - }; - }); - }, - [setNewCollectionPath] - ); - - useEffect(() => { - getWorkspaces().then(w => { - if (workspaceId) { - setWorkspaces(w.filter(i => i.id === workspaceId)); - } else { - setWorkspaces(w); - } - }); - }, [workspaceId]); - - const handleToggle = (_event: React.ChangeEvent<{}>, nodeIds: string[]) => { - setExpanded(nodeIds); - }; - - if (!workspaces) { - return ; - } - - return ( - } - defaultExpandIcon={} - expanded={expanded} - selected={selected as any} - onNodeToggle={handleToggle} - onNodeSelect={handleSelect as any} - multiSelect={multiple || false} - > - {workspaces.map(w => { - const nodeId = w.id + nodeSeparator + w['@id']; - return ( - - - - {w.name} - - - - - } - disabled={ - disabledBranches && - disabledBranches.some(b => nodeId.startsWith(b)) - } - > - {w.collections.map(c => ( - - ))} - - ); - })} - - ); -} diff --git a/databox/client/src/components/Media/Collection/EditableTree.tsx b/databox/client/src/components/Media/Collection/EditableTree.tsx index 5105c6e2b..0e79b2ac4 100644 --- a/databox/client/src/components/Media/Collection/EditableTree.tsx +++ b/databox/client/src/components/Media/Collection/EditableTree.tsx @@ -12,7 +12,7 @@ import { NewCollectionNodeState, SetExpanded, UpdateCollectionPath, -} from './CollectionsTreeView'; +} from './CollectionTree/collectionTree.ts'; type Props = { offset: number; diff --git a/databox/client/src/components/Media/CollectionMenuItem.tsx b/databox/client/src/components/Media/CollectionMenuItem.tsx index 4553248b6..c8141db73 100644 --- a/databox/client/src/components/Media/CollectionMenuItem.tsx +++ b/databox/client/src/components/Media/CollectionMenuItem.tsx @@ -1,5 +1,5 @@ import React, {MouseEvent, useContext, useState} from 'react'; -import {Collection} from '../../types'; +import {Collection, Workspace} from '../../types'; import {SearchContext} from './Search/SearchContext'; import { CircularProgress, @@ -35,20 +35,20 @@ import {cActionClassName} from './WorkspaceMenuItem'; type Props = { level: number; - workspaceId: string; absolutePath: string; titlePath?: string[]; - data: Collection; + collection: Collection; + workspace: Workspace; }; export const collectionItemClassName = 'collection-item'; export default function CollectionMenuItem({ - data, + collection, absolutePath, titlePath, level, - workspaceId, + workspace, }: Props) { const {t} = useTranslation(); const {openModal} = useModals(); @@ -56,33 +56,31 @@ export default function CollectionMenuItem({ const authContext = useAuth(); const [expanded, setExpanded] = useState(false); const [childrenLoaded, setChildrenLoaded] = React.useState(false); - const childCount = data.children?.length ?? 0; + const childCount = collection.children?.length ?? 0; - const loadChildren = useCollectionStore(state => state.loadChildren); + const load = useCollectionStore(state => state.load); const addCollection = useCollectionStore(state => state.addCollection); const loadMore = useCollectionStore(state => state.loadMore); useCollectionStore(state => state.collections); // Subscribe to collection updates const pager = - useCollectionStore(state => state.tree)[data.id] ?? + useCollectionStore(state => state.tree)[collection.id] ?? ({ - items: data.children, + items: collection.children, expanding: false, loadingMore: false, } as CollectionPager); - const {workspace} = data; - React.useEffect(() => { if (expanded && !childrenLoaded && childCount > 0) { - loadChildren(workspaceId, data.id).then(() => { + load(workspace.id, collection.id).then(() => { setChildrenLoaded(true); }); } }, [expanded, childrenLoaded]); const expand = (force?: boolean) => { - setExpanded(p => !p || true === force); + setExpanded(p => !p || !!force); }; const expandClick = (e: MouseEvent) => { e.stopPropagation(); @@ -90,7 +88,7 @@ export default function CollectionMenuItem({ if (e.detail > 1) { // is double click - loadChildren(workspaceId, data.id); + load(workspace.id, collection.id); } }; @@ -98,13 +96,13 @@ export default function CollectionMenuItem({ e.stopPropagation(); openModal(ConfirmDialog, { - textToType: data.title, + textToType: collection.title, title: t( 'collection_delete.confirm', 'Are you sure you want to delete this collection?' ), onConfirm: async () => { - await deleteCollection(data.id); + await deleteCollection(collection.id); toast.success( t( 'delete.collection.confirmed', @@ -119,7 +117,7 @@ export default function CollectionMenuItem({ const onClick = () => { searchContext.selectCollection( absolutePath, - (titlePath ?? []).concat(data.title).join(` / `), + (titlePath ?? []).concat(collection.title).join(` / `), selected ); expand(true); @@ -136,7 +134,7 @@ export default function CollectionMenuItem({ secondaryAction={ <> - {data.capabilities.canEdit && + {collection.capabilities.canEdit && authContext!.isAuthenticated() ? ( openModal(CreateCollection, { - parent: data['@id'], + parent: collection['@id'], workspaceTitle: workspace.name, titlePath: (titlePath ?? []).concat( - data.title + collection.title ), onCreate: coll => { addCollection( coll, - workspaceId, - data.id + workspace.id, + collection.id ); expand(true); }, @@ -189,14 +187,14 @@ export default function CollectionMenuItem({ )} - {data.capabilities.canEdit && ( + {collection.capabilities.canEdit && ( )} - {data.capabilities.canDelete && ( + {collection.capabilities.canDelete && ( - {data.public ? ( + {collection.public ? ( - ) : data.shared ? ( + ) : collection.shared ? ( ) : ( )} - + @@ -266,12 +264,12 @@ export default function CollectionMenuItem({ {pager?.items.map(c => { return ( @@ -280,7 +278,7 @@ export default function CollectionMenuItem({ {pager && pager.items.length < (pager.total ?? 0) && ( - loadMore(workspaceId, data.id) + loadMore(workspace.id, collection.id) } loading={pager.loadingMore} /> diff --git a/databox/client/src/components/Media/CollectionsPanel.tsx b/databox/client/src/components/Media/CollectionsPanel.tsx index 7a5039918..1e0c13cfa 100644 --- a/databox/client/src/components/Media/CollectionsPanel.tsx +++ b/databox/client/src/components/Media/CollectionsPanel.tsx @@ -1,28 +1,23 @@ import React from 'react'; -import {useCollectionStore} from '../../store/collectionStore'; import WorkspaceMenuItem, { cActionClassName, workspaceItemClassName, } from './WorkspaceMenuItem'; -import {getWorkspaces} from '../../api/collection'; -import {Workspace} from '../../types'; -import {alpha, Box} from '@mui/material'; +import {alpha, Box, CircularProgress} from '@mui/material'; import {collectionItemClassName} from './CollectionMenuItem'; +import {useWorkspaceStore} from '../../store/workspaceStore.ts'; +import useEffectOnce from '@alchemy/react-hooks/src/useEffectOnce'; +import {FlexRow} from '@alchemy/phrasea-ui'; type Props = {}; function CollectionsPanel({}: Props) { - const [workspaces, setWorkspaces] = React.useState([]); + const loadWorkspaces = useWorkspaceStore(state => state.load); + const loading = useWorkspaceStore(state => state.loading); + const workspaces = useWorkspaceStore(state => state.workspaces); - const setRootCollections = useCollectionStore( - state => state.setRootCollections - ); - - React.useEffect(() => { - getWorkspaces().then(result => { - setRootCollections(result); - setWorkspaces(result); - }); + useEffectOnce(() => { + loadWorkspaces(); }, []); return ( @@ -42,6 +37,7 @@ function CollectionsPanel({}: Props) { }, [`.MuiListItemButton-root.Mui-selected`]: { backgroundColor: theme.palette.secondary.main, + color: theme.palette.secondary.contrastText, }, }, '.MuiListItemIcon-root': { @@ -65,9 +61,22 @@ function CollectionsPanel({}: Props) { }, })} > - {workspaces.map(w => ( - - ))} + {loading ? ( + + + + ) : ( + <> + {workspaces?.map(w => ( + + ))} + + )} ); } diff --git a/databox/client/src/components/Media/LeftPanel.tsx b/databox/client/src/components/Media/LeftPanel.tsx index dc153a84a..bdf5e3c95 100644 --- a/databox/client/src/components/Media/LeftPanel.tsx +++ b/databox/client/src/components/Media/LeftPanel.tsx @@ -2,7 +2,6 @@ import React, {useState} from 'react'; import Facets from './Asset/Facets'; import CollectionsPanel from './CollectionsPanel'; import {Tab, Tabs} from '@mui/material'; -import {styled} from '@mui/material/styles'; import {TabPanelProps} from '@mui/lab'; import BasketsPanel from '../Basket/BasketsPanel'; import {useAuth} from '@alchemy/react-auth'; @@ -29,73 +28,55 @@ function TabPanel(props: {index: string} & TabPanelProps) { ); } -const AntTabs = styled(Tabs)({ - root: { - backgroundColor: 'none', - borderBottom: '1px solid #e8e8e8', - }, - indicator: { - backgroundColor: '#1890ff', - }, -}); - -const AntTab = styled(Tab)({ - root: { - '&:hover': { - color: '#40a9ff', - opacity: 1, - }, - '&$selected': { - color: '#1890ff', - }, - '&:focus': { - color: '#40a9ff', - }, - }, - selected: {}, -}); - export default function LeftPanel() { const {t} = useTranslation(); - const [tab, setTab] = useState(TabEnum.tree); + const [tab, setTab] = useState(TabEnum.facets); const {isAuthenticated} = useAuth(); + const treeLoadedOnce = React.useRef(false); const handleChange = (_event: React.ChangeEvent<{}>, newValue: TabEnum) => { + if (newValue === TabEnum.tree && !treeLoadedOnce.current) { + treeLoadedOnce.current = true; + } setTab(newValue); }; return ( <> - - - + + {isAuthenticated() ? ( - ) : null} - - - - + + + {treeLoadedOnce.current || tab === TabEnum.tree ? ( + + ) : ( + '' + )} + {isAuthenticated() ? ( diff --git a/databox/client/src/components/Media/Search/SearchFilters.tsx b/databox/client/src/components/Media/Search/SearchFilters.tsx index 978d5cb5d..8fdb000ee 100644 --- a/databox/client/src/components/Media/Search/SearchFilters.tsx +++ b/databox/client/src/components/Media/Search/SearchFilters.tsx @@ -50,11 +50,39 @@ function formatFilterTitle( .map(v => extractLabelValueFromKey(v, type).label) .join(`" ${t('common.or', `or`)} "`)}"`; case FacetType.DateRange: - return `${title} between ${ - extractLabelValueFromKey(value[0], type, DateFormats.Long).label - } and ${ - extractLabelValueFromKey(value[1], type, DateFormats.Long).label - }`; + if (value[0] && value[1]) { + return t('filter.between', { + defaultValue: '{{title}} between {{from}} and {{to}}', + title, + from: extractLabelValueFromKey( + value[0], + type, + DateFormats.Long + ).label, + to: extractLabelValueFromKey( + value[1], + type, + DateFormats.Long + ).label, + }); + } else if (value[0]) { + return t('filter.after', { + defaultValue: '{{title}} after {{from}}', + title, + from: extractLabelValueFromKey( + value[0], + type, + DateFormats.Long + ).label, + }); + } + + return t('filter.before', { + defaultValue: '{{title}} before {{to}}', + title, + to: extractLabelValueFromKey(value[1], type, DateFormats.Long) + .label, + }); } } @@ -78,13 +106,25 @@ function formatFilterLabel( .map(s => truncate(extractLabelValueFromKey(s, type).label, 15)) .join(', '); case FacetType.DateRange: - return `${ - extractLabelValueFromKey(value[0], type, DateFormats.Short) - .label - } - ${ - extractLabelValueFromKey(value[1], type, DateFormats.Short) - .label - }`; + if (value[0] && value[1]) { + return `${ + extractLabelValueFromKey(value[0], type, DateFormats.Short) + .label + } - ${ + extractLabelValueFromKey(value[1], type, DateFormats.Short) + .label + }`; + } else if (value[0]) { + return `>= ${ + extractLabelValueFromKey(value[0], type, DateFormats.Short) + .label + }`; + } else { + return `<= ${ + extractLabelValueFromKey(value[1], type, DateFormats.Short) + .label + }`; + } } } diff --git a/databox/client/src/components/Media/Search/search.ts b/databox/client/src/components/Media/Search/search.ts index b9f01a4e0..237932007 100644 --- a/databox/client/src/components/Media/Search/search.ts +++ b/databox/client/src/components/Media/Search/search.ts @@ -73,7 +73,7 @@ function decodeFilter(str: string): FilterEntry { function normalizeBucketValue( v: ResolvedBucketValue ): NormalizedBucketKeyValue { - if (typeof v === 'object') { + if (v && typeof v === 'object') { return { v: v.value, l: v.label, @@ -86,7 +86,7 @@ function normalizeBucketValue( function denormalizeBucketValue( v: NormalizedBucketKeyValue ): ResolvedBucketValue { - if (typeof v === 'object') { + if (v && typeof v === 'object') { return { value: v.v, label: v.l, diff --git a/databox/client/src/components/Media/WorkspaceMenuItem.tsx b/databox/client/src/components/Media/WorkspaceMenuItem.tsx index bcf29dc45..657d9e6f4 100644 --- a/databox/client/src/components/Media/WorkspaceMenuItem.tsx +++ b/databox/client/src/components/Media/WorkspaceMenuItem.tsx @@ -22,8 +22,7 @@ import ModalLink from '../Routing/ModalLink'; import {useTranslation} from 'react-i18next'; import {useModals} from '@alchemy/navigation'; import {modalRoutes} from '../../routes'; -import {useCollectionStore} from '../../store/collectionStore'; -import {useShallow} from 'zustand/react/shallow'; +import {CollectionPager, useCollectionStore} from '../../store/collectionStore'; import LoadMoreCollections from './Collection/LoadMoreCollections'; export type WorkspaceMenuItemProps = { @@ -44,8 +43,15 @@ export default function WorkspaceMenuItem({data}: WorkspaceMenuItemProps) { const addCollection = useCollectionStore(state => state.addCollection); const loadMore = useCollectionStore(state => state.loadMore); - const loadRoot = useCollectionStore(state => state.loadRoot); - const pager = useCollectionStore(useShallow(state => state.tree))[id]; + const loadRoot = useCollectionStore(state => state.load); + + const pager = + useCollectionStore(state => state.tree)[id] ?? + ({ + items: [], + expanding: false, + loadingMore: false, + } as CollectionPager); const expand = (force?: boolean) => { setExpanded(p => !p || true === force); @@ -54,8 +60,10 @@ export default function WorkspaceMenuItem({data}: WorkspaceMenuItemProps) { e.stopPropagation(); expand(); - if (e.detail > 1) { - // is double click + if ( + undefined === pager.total || + e.detail > 1 // is double click + ) { loadRoot(id); } }; @@ -118,7 +126,10 @@ export default function WorkspaceMenuItem({data}: WorkspaceMenuItemProps) { aria-label="expand-toggle" > {pager.expanding ? ( - + ) : !expanded ? ( ) : ( @@ -146,11 +157,11 @@ export default function WorkspaceMenuItem({data}: WorkspaceMenuItemProps) { {pager?.items && pager!.items.map(c => ( ))} {pager && pager.items.length < (pager.total ?? 0) && ( diff --git a/databox/client/src/components/Share/CreateShareDialog.tsx b/databox/client/src/components/Share/CreateShareDialog.tsx index f19953342..b1010dccb 100644 --- a/databox/client/src/components/Share/CreateShareDialog.tsx +++ b/databox/client/src/components/Share/CreateShareDialog.tsx @@ -3,15 +3,11 @@ import {Asset, Share} from '../../types.ts'; import {FormFieldErrors, FormRow} from '@alchemy/react-form'; import {TextField} from '@mui/material'; import FormDialog from '../Dialog/FormDialog.tsx'; -import { - StackedModalProps, - useModals, - useFormPrompt, -} from '@alchemy/navigation'; +import {StackedModalProps, useModals, useFormPrompt} from '@alchemy/navigation'; import {createAssetShare} from '../../api/asset.ts'; import {useFormSubmit} from '../../../../../lib/js/api'; import RemoteErrors from '../Form/RemoteErrors.tsx'; -import {normalizeDate} from "../../lib/date.ts"; +import {normalizeDate} from '../../lib/date.ts'; type Props = { asset: Asset; diff --git a/databox/client/src/components/Share/EmbedDialog.tsx b/databox/client/src/components/Share/EmbedDialog.tsx index 2b033e75c..143c1f686 100644 --- a/databox/client/src/components/Share/EmbedDialog.tsx +++ b/databox/client/src/components/Share/EmbedDialog.tsx @@ -1,12 +1,12 @@ -import {Box, Button, IconButton, TextField} from "@mui/material"; -import {AppDialog} from "@alchemy/phrasea-ui"; -import {StackedModalProps, useModals} from "@alchemy/navigation"; -import {useTranslation} from "react-i18next"; -import React from "react"; -import CloseIcon from "@mui/icons-material/Close"; +import {Box, Button, IconButton, TextField} from '@mui/material'; +import {AppDialog} from '@alchemy/phrasea-ui'; +import {StackedModalProps, useModals} from '@alchemy/navigation'; +import {useTranslation} from 'react-i18next'; +import React from 'react'; +import CloseIcon from '@mui/icons-material/Close'; import RestartAltIcon from '@mui/icons-material/RestartAlt'; -import CopyToClipboard from "../../lib/CopyToClipboard.tsx"; -import ContentCopyIcon from "@mui/icons-material/ContentCopy"; +import CopyToClipboard from '../../lib/CopyToClipboard.tsx'; +import ContentCopyIcon from '@mui/icons-material/ContentCopy'; export type EmbedProps = { url: string; @@ -27,8 +27,8 @@ export default function EmbedDialog({ const handleFocus = (event: React.FocusEvent) => event.currentTarget.select(); - const defaultCode = isImage ? - `${title}` + const defaultCode = isImage + ? `${title}` : ``; const [code, setCode] = React.useState(defaultCode); @@ -45,25 +45,21 @@ export default function EmbedDialog({ maxWidth={'md'} actions={({onClose}) => ( <> - - )} > - + - ({ - position: 'absolute', - bottom: theme.spacing(1), - right: theme.spacing(1), - })}> + ({ + position: 'absolute', + bottom: theme.spacing(1), + right: theme.spacing(1), + })} + > {({copy}) => ( -
- +
- ); + ); } diff --git a/databox/client/src/components/Share/SelectShareAlternateUrl.tsx b/databox/client/src/components/Share/SelectShareAlternateUrl.tsx index 4b114bbc9..cd476d4b3 100644 --- a/databox/client/src/components/Share/SelectShareAlternateUrl.tsx +++ b/databox/client/src/components/Share/SelectShareAlternateUrl.tsx @@ -1,8 +1,8 @@ -import React from "react"; +import React from 'react'; import {useTranslation} from 'react-i18next'; -import {Button, Menu, MenuItem} from "@mui/material"; -import {ShareAlternateUrl} from "../../types.ts"; -import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import {Button, Menu, MenuItem} from '@mui/material'; +import {ShareAlternateUrl} from '../../types.ts'; +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; type Props = { onSelect: (value: string | undefined) => void; @@ -25,44 +25,39 @@ export default function SelectShareAlternateUrl({ setAnchorEl(null); }; - const select: Props['onSelect'] = (value) => { + const select: Props['onSelect'] = value => { onSelect(value); handleClose(); - } + }; const defaultLabel = t('share.item.rendition.asset', 'Asset'); - return <> - - - select(undefined)} - selected={!value} + return ( + <> + + + select(undefined)} selected={!value}> + {defaultLabel} - ))} - - + {alternateUrls.map(a => ( + select(a.name)} + selected={a.name === value} + > + {a.name} + + ))} + + + ); } diff --git a/databox/client/src/components/Share/ShareAssetDialog.tsx b/databox/client/src/components/Share/ShareAssetDialog.tsx index 3c77c6589..80cd3d214 100644 --- a/databox/client/src/components/Share/ShareAssetDialog.tsx +++ b/databox/client/src/components/Share/ShareAssetDialog.tsx @@ -29,9 +29,9 @@ import CopiableTextField from '../Ui/CopiableTextField.tsx'; import {toast} from 'react-toastify'; import ShareItem from './ShareItem.tsx'; import {StackedModalProps, useModals} from '@alchemy/navigation'; -import {getShareTitle, UrlActions} from "./UrlActions.tsx"; -import {getShareUrl} from "./shareUtils.ts"; -import ShareSocials from "./ShareSocials.tsx"; +import {getShareTitle, UrlActions} from './UrlActions.tsx'; +import {getShareUrl} from './shareUtils.ts'; +import ShareSocials from './ShareSocials.tsx'; type Props = { asset: Asset; @@ -192,9 +192,7 @@ export default function ShareAssetDialog({asset, open, modalIndex}: Props) { } + actions={} />
(); + const [selectedAlternate, setSelectedAlternate] = React.useState< + string | undefined + >(); const shareTitle = getShareTitle(share); - const alternateUrl = selectedAlternate ? share.alternateUrls.find(a => a.name === selectedAlternate) : undefined; + const alternateUrl = selectedAlternate + ? share.alternateUrls.find(a => a.name === selectedAlternate) + : undefined; const shareUrl = alternateUrl?.url ?? getShareUrl(share); return ( @@ -47,10 +51,7 @@ export default function ShareItem({share, revoking, onRevoke}: Props) { '' )} - + {t( 'share.item.createdAt', 'Created at {{date}}', @@ -66,17 +67,18 @@ export default function ShareItem({share, revoking, onRevoke}: Props) { - -
} - actions={} + startAdornment={ +
+ +
+ } + actions={} />
@@ -90,7 +92,7 @@ export default function ShareItem({share, revoking, onRevoke}: Props) { mr: 1, }} > - + {t( 'share.item.startsAt', @@ -113,7 +115,7 @@ export default function ShareItem({share, revoking, onRevoke}: Props) { mr: 1, }} > - + {t( 'share.item.expiresAt', @@ -132,7 +134,11 @@ export default function ShareItem({share, revoking, onRevoke}: Props) {
@@ -148,7 +154,7 @@ export default function ShareItem({share, revoking, onRevoke}: Props) { ml: 2, }} color={'error'} - startIcon={} + startIcon={} loading={revoking} disabled={revoking} onClick={() => onRevoke(share.id)} diff --git a/databox/client/src/components/Share/ShareSocials.tsx b/databox/client/src/components/Share/ShareSocials.tsx index 479d71a9b..978d29209 100644 --- a/databox/client/src/components/Share/ShareSocials.tsx +++ b/databox/client/src/components/Share/ShareSocials.tsx @@ -21,12 +21,12 @@ import { WorkplaceIcon, WorkplaceShareButton, XIcon, -} from "react-share"; -import {Box, IconButton} from "@mui/material"; +} from 'react-share'; +import {Box, IconButton} from '@mui/material'; import {useTranslation} from 'react-i18next'; -import EmbedDialog, {EmbedProps} from "./EmbedDialog.tsx"; -import CodeIcon from "@mui/icons-material/Code"; -import {useModals} from "@alchemy/navigation"; +import EmbedDialog, {EmbedProps} from './EmbedDialog.tsx'; +import CodeIcon from '@mui/icons-material/Code'; +import {useModals} from '@alchemy/navigation'; type Props = EmbedProps; @@ -36,104 +36,136 @@ export default function ShareSocials(props: Props) { const iconSize = 32; const {t} = useTranslation(); - return <> - ({ - mt: theme.spacing(1), - display: 'flex', - flexWrap: 'wrap', - alignItems: 'center', - direction: 'row', - gap: theme.spacing(1), - 'button svg': { - display: 'block', - } - })} - > - { - openModal(EmbedDialog, props); - }} + return ( + <> + ({ + 'mt': theme.spacing(1), + 'display': 'flex', + 'flexWrap': 'wrap', + 'alignItems': 'center', + 'direction': 'row', + 'gap': theme.spacing(1), + 'button svg': { + display: 'block', + }, + })} > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + { + openModal(EmbedDialog, props); + }} + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + ); } diff --git a/databox/client/src/components/Share/UrlActions.tsx b/databox/client/src/components/Share/UrlActions.tsx index 1d838a01b..2a8625adf 100644 --- a/databox/client/src/components/Share/UrlActions.tsx +++ b/databox/client/src/components/Share/UrlActions.tsx @@ -1,7 +1,7 @@ -import {FlexRow} from "../../../../../lib/js/phrasea-ui"; -import {IconButton} from "@mui/material"; -import OpenInNewIcon from "@mui/icons-material/OpenInNew"; -import {Share} from "../../types.ts"; +import {FlexRow} from '../../../../../lib/js/phrasea-ui'; +import {IconButton} from '@mui/material'; +import OpenInNewIcon from '@mui/icons-material/OpenInNew'; +import {Share} from '../../types.ts'; type Props = { url: string; @@ -10,11 +10,8 @@ type Props = { export function UrlActions({url}: Props) { return ( - - + + ); diff --git a/databox/client/src/components/Share/shareUtils.ts b/databox/client/src/components/Share/shareUtils.ts index 76a0d1479..4613f0886 100644 --- a/databox/client/src/components/Share/shareUtils.ts +++ b/databox/client/src/components/Share/shareUtils.ts @@ -1,6 +1,6 @@ -import {Share} from "../../types.ts"; -import {routes} from "../../routes.ts"; -import {getPath} from "@alchemy/navigation"; +import {Share} from '../../types.ts'; +import {routes} from '../../routes.ts'; +import {getPath} from '@alchemy/navigation'; export function getShareUrl(s: Share) { return getPath( @@ -14,4 +14,3 @@ export function getShareUrl(s: Share) { } ); } - diff --git a/databox/client/src/components/Ui/CopiableTextField.tsx b/databox/client/src/components/Ui/CopiableTextField.tsx index cb027d5d7..4e51c968c 100644 --- a/databox/client/src/components/Ui/CopiableTextField.tsx +++ b/databox/client/src/components/Ui/CopiableTextField.tsx @@ -15,7 +15,12 @@ type Props = { value: string; } & Omit; -export default function CopiableTextField({value, actions, startAdornment, ...props}: Props) { +export default function CopiableTextField({ + value, + actions, + startAdornment, + ...props +}: Props) { const handleFocus = (event: React.FocusEvent) => event.currentTarget.select(); @@ -26,9 +31,11 @@ export default function CopiableTextField({value, actions, startAdornment, ...pr onFocus={handleFocus} InputProps={{ readOnly: true, - startAdornment: startAdornment ? - {startAdornment} - : undefined, + startAdornment: startAdornment ? ( + + {startAdornment} + + ) : undefined, endAdornment: ( diff --git a/databox/client/src/components/Upload/UploadForm.tsx b/databox/client/src/components/Upload/UploadForm.tsx index 2db2f7aa1..51f272098 100644 --- a/databox/client/src/components/Upload/UploadForm.tsx +++ b/databox/client/src/components/Upload/UploadForm.tsx @@ -12,7 +12,6 @@ import { buildAttributeIndex, useAttributeEditor, } from '../Media/Asset/Attribute/useAttributeEditor'; -import {Collection} from '../Media/Collection/CollectionsTreeView'; import SaveAsTemplateForm from './SaveAsTemplateForm'; import {useAssetDataTemplateOptions} from '../Media/Asset/Attribute/useAssetDataTemplateOptions'; import {AssetDataTemplate, getAssetDataTemplate} from '../../api/templates'; @@ -24,6 +23,7 @@ import FullPageLoader from '../Ui/FullPageLoader'; import {useFormPrompt} from '@alchemy/navigation'; import {UseFormSubmitReturn} from '@alchemy/api'; import {WorkspaceContext} from '../../context/WorkspaceContext.tsx'; +import {Collection} from '../Media/Collection/CollectionTree/collectionTree.ts'; export type UploadData = { destination: Collection; diff --git a/databox/client/src/components/Upload/UploadModal.tsx b/databox/client/src/components/Upload/UploadModal.tsx index 749ce0a55..3849d35c0 100644 --- a/databox/client/src/components/Upload/UploadModal.tsx +++ b/databox/client/src/components/Upload/UploadModal.tsx @@ -12,7 +12,6 @@ import moment from 'moment'; import {v4 as uuidv4} from 'uuid'; import UploadDropzone from './UploadDropzone'; import {CollectionChip, WorkspaceChip} from '../Ui/Chips'; -import {CollectionId} from '../Media/Collection/CollectionsTreeView'; import {useAttributeEditor} from '../Media/Asset/Attribute/useAttributeEditor'; import {useAssetDataTemplateOptions} from '../Media/Asset/Attribute/useAssetDataTemplateOptions'; import { @@ -20,15 +19,12 @@ import { postAssetDataTemplate, putAssetDataTemplate, } from '../../api/templates'; -import { - StackedModalProps, - useModals, - useFormPrompt, -} from '@alchemy/navigation'; +import {StackedModalProps, useModals, useFormPrompt} from '@alchemy/navigation'; import {Privacy} from '../../api/privacy'; import {Asset} from '../../types'; import {getAttributeList} from '../Media/Asset/Attribute/AttributeListData.ts'; import type {TFunction} from '@alchemy/i18n'; +import {CollectionId} from '../Media/Collection/CollectionTree/collectionTree.ts'; type FileWrapper = { id: string; diff --git a/databox/client/src/components/User/Preferences/UserPreferencesContext.ts b/databox/client/src/components/User/Preferences/UserPreferencesContext.ts index bd6a7ac47..57ebed602 100644 --- a/databox/client/src/components/User/Preferences/UserPreferencesContext.ts +++ b/databox/client/src/components/User/Preferences/UserPreferencesContext.ts @@ -8,11 +8,14 @@ export type UserPreferences = { layout?: Layout; }; +export type UpdatePreferenceHandlerArg = + | ((prev: UserPreferences[T]) => UserPreferences[T]) + | UserPreferences[T] + | undefined; + export type UpdatePreferenceHandler = ( name: T, - handler: - | ((prev: UserPreferences[T]) => UserPreferences[T]) - | UserPreferences[T] + handler: UpdatePreferenceHandlerArg ) => void; export type TUserPreferencesContext = { diff --git a/databox/client/src/components/User/Preferences/UserPreferencesProvider.tsx b/databox/client/src/components/User/Preferences/UserPreferencesProvider.tsx index 53e7efb08..3b0a848d2 100644 --- a/databox/client/src/components/User/Preferences/UserPreferencesProvider.tsx +++ b/databox/client/src/components/User/Preferences/UserPreferencesProvider.tsx @@ -2,6 +2,7 @@ import React, {PropsWithChildren} from 'react'; import { TUserPreferencesContext, UpdatePreferenceHandler, + UpdatePreferenceHandlerArg, UserPreferences, UserPreferencesContext, } from './UserPreferencesContext'; @@ -12,6 +13,17 @@ import {useAuth} from '@alchemy/react-auth'; import {ThemeEditorProvider} from '@alchemy/theme-editor'; import {Classes} from '../../../classes.ts'; import {scrollbarWidth} from '../../../constants.ts'; +import {FullPageLoader} from '@alchemy/phrasea-ui'; +import {useTranslation} from 'react-i18next'; +import {useMutation, useQuery} from '@tanstack/react-query'; +import {queryClient} from '../../../lib/query.ts'; + +type UpdatePrefVariables< + T extends keyof UserPreferences = keyof UserPreferences, +> = { + name: T; + handler: UpdatePreferenceHandlerArg; +}; const sessionStorageKey = 'userPrefs'; @@ -25,53 +37,73 @@ function getFromStorage(): UserPreferences { return {}; } +function putToStorage(prefs: UserPreferences): void { + sessionStorage.setItem(sessionStorageKey, JSON.stringify(prefs)); +} + type Props = PropsWithChildren<{}>; export default function UserPreferencesProvider({children}: Props) { - const [preferences, setPreferences] = - React.useState(getFromStorage()); const {user} = useAuth(); + const {t} = useTranslation(); + const queryKey = ['userPreferences', user?.id]; + + const {data: preferences, isLoading} = useQuery({ + initialData: getFromStorage(), + staleTime: 0, + refetchOnWindowFocus: false, + queryFn: async () => { + const userPreferences = await getUserPreferences(); + putToStorage(userPreferences); + + return userPreferences; + }, + queryKey, + enabled: !!user, + }); - const updatePreference = React.useCallback( - (name, value) => { - setPreferences(prev => { - const newPrefs = {...prev}; + const mutationFn = async ({ + name, + handler, + }: UpdatePrefVariables): Promise => { + return queryClient.setQueryData(queryKey, prev => { + const newPrefs = {...(prev ?? {})} as UserPreferences; - if (typeof value === 'function') { - newPrefs[name] = value(newPrefs[name]); - } else { - newPrefs[name] = value; - } + if (typeof handler === 'function') { + newPrefs[name] = handler(newPrefs[name]); + } else { + newPrefs[name] = handler; + } - if (user) { + if (user) { + setTimeout(() => { putUserPreferences(name, newPrefs[name]); - } + }, 0); + } - sessionStorage.setItem( - sessionStorageKey, - JSON.stringify(newPrefs) - ); + putToStorage(newPrefs); - return newPrefs; - }); - }, - [user] - ); + return newPrefs; + })!; + }; - React.useEffect(() => { - if (user) { - getUserPreferences().then(r => - setPreferences({ - ...r, - }) - ); - } - }, [user?.id]); + const updatePreference = useMutation< + UserPreferences, + {}, + UpdatePrefVariables + >({ + mutationFn, + }); const value = React.useMemo(() => { return { preferences, - updatePreference, + updatePreference: ((name, handler) => { + updatePreference.mutate({ + name, + handler, + }); + }) as UpdatePreferenceHandler, }; }, [preferences, updatePreference]); @@ -110,7 +142,18 @@ export default function UserPreferencesProvider({children}: Props) { }, })} /> - {children} + + {!isLoading ? ( + children + ) : ( + + )} ); diff --git a/databox/client/src/lib/date.ts b/databox/client/src/lib/date.ts index c96ea78b8..13bd5bef2 100644 --- a/databox/client/src/lib/date.ts +++ b/databox/client/src/lib/date.ts @@ -1,5 +1,6 @@ - -export function normalizeDate(date: string | null | undefined): string | null | undefined { +export function normalizeDate( + date: string | null | undefined +): string | null | undefined { if (date) { return new Date(date).toISOString(); } diff --git a/databox/client/src/lib/upload/uploader.ts b/databox/client/src/lib/upload/uploader.ts index 212faf66e..196fd4b9d 100644 --- a/databox/client/src/lib/upload/uploader.ts +++ b/databox/client/src/lib/upload/uploader.ts @@ -1,8 +1,3 @@ -import { - CollectionId, - NewCollectionPath, - treeViewPathSeparator, -} from '../../components/Media/Collection/CollectionsTreeView'; import {postCollection} from '../../api/collection'; import {UploadFiles} from '../../api/uploader/file'; import {Asset} from '../../types'; @@ -12,6 +7,11 @@ import { postMultipleAssets, } from '../../api/asset'; import {v4 as uuidv4} from 'uuid'; +import { + CollectionId, + NewCollectionPath, + treeViewPathSeparator, +} from '../../components/Media/Collection/CollectionTree/collectionTree.ts'; type InputFile = { title?: string; diff --git a/databox/client/src/store/basketStore.ts b/databox/client/src/store/basketStore.ts index d4865310c..14ee1beaf 100644 --- a/databox/client/src/store/basketStore.ts +++ b/databox/client/src/store/basketStore.ts @@ -14,12 +14,13 @@ type State = { baskets: Basket[]; current: Basket | undefined; nextUrl?: string | undefined; + loaded: boolean; loading: boolean; loadingCurrent: boolean; loadingMore: boolean; total?: number; hasMore: () => boolean; - load: (params?: GetBasketOptions) => Promise; + load: (params?: GetBasketOptions, force?: boolean) => Promise; loadMore: () => Promise; addBasket: (basket: Basket) => void; updateBasket: (data: Basket) => void; @@ -32,14 +33,19 @@ type State = { export const useBasketStore = create((set, getState) => ({ loadingMore: false, + loaded: false, loading: false, loadingCurrent: false, current: undefined, baskets: [], - load: async params => { + load: async (params, force) => { + if (getState().loaded && !force) { + return; + } + set({ - loading: false, + loading: true, }); try { @@ -53,7 +59,7 @@ export const useBasketStore = create((set, getState) => ({ nextUrl: data.next || undefined, })); } catch (e: any) { - set({loadingMore: true}); + set({loading: true}); throw e; } }, diff --git a/databox/client/src/store/collectionStore.ts b/databox/client/src/store/collectionStore.ts index b6cb5cc4b..476685b91 100644 --- a/databox/client/src/store/collectionStore.ts +++ b/databox/client/src/store/collectionStore.ts @@ -1,5 +1,5 @@ -import {create} from 'zustand'; -import {Collection, Workspace} from '../types'; +import {create, StoreApi} from 'zustand'; +import {Collection} from '../types'; import { collectionChildrenLimit, CollectionOptions, @@ -19,14 +19,19 @@ type CollectionExtended = { parentId: string | undefined; } & Collection; +type JustCreatedIndex = Record>; + type State = { collections: Record; + justCreated: JustCreatedIndex; tree: Record; - setRootCollections: (workspaces: Workspace[]) => void; updateCollection: (collection: Collection) => void; partialUpdateCollection: (id: string, updates: Partial) => void; - loadChildren: (workspaceId: string, parentId: string) => Promise; - loadRoot: (workspaceId: string) => Promise; + load: ( + workspaceId: string, + parentId?: string, + force?: boolean + ) => Promise; loadMore: (workspaceId: string, parentId?: string) => Promise; addCollection: ( collection: Collection, @@ -37,44 +42,28 @@ type State = { moveCollection: (id: string, to: string | undefined) => void; }; -function updateCollectionByReference( - state: Record, - collection: Collection | CollectionExtended, - workspaceId: string, - parentId?: string | undefined -): CollectionExtended { - const c = state[collection.id]; - - if (c) { - (Object.keys(collection) as (keyof typeof collection)[]).forEach(k => { - // @ts-expect-error key typing - c[k] = collection[k]; - }); - } else { - return (state[collection.id] = { - ...collection, - workspaceId, - parentId, - } as CollectionExtended); - } - - return c as CollectionExtended; -} - export const useCollectionStore = create((set, getState) => ({ collections: {}, tree: {}, + justCreated: {}, + + load: async (workspaceId, parentId, force) => { + const pagerId = parentId ?? workspaceId; + if (!force && getState().tree[pagerId]) { + return; + } - loadChildren: async (workspaceId, parentId) => { const timeout = setTimeout(() => { - set(createPagerExpandingSetter(parentId)); + set(createPagerExpandingSetter(pagerId, parentId)); }, 800); const data = await getCollections({ + workspaces: [workspaceId], parent: parentId, limit: collectionSecondLimit, childrenLimit: collectionChildrenLimit, }); + clearTimeout(timeout); set(state => { @@ -90,47 +79,20 @@ export const useCollectionStore = create((set, getState) => ({ ); }); - tree[parentId] = { - ...(tree[parentId] ?? {}), - items, - total: data.total, - expanding: false, - loadingMore: false, - }; - - return { - tree, - collections: newCollections, - }; - }); - }, - - loadRoot: async workspaceId => { - const timeout = setTimeout(() => { - set(createPagerExpandingSetter(workspaceId)); - }, 800); - - const data = await getCollections({ - workspaces: [workspaceId], - limit: collectionSecondLimit, - childrenLimit: collectionChildrenLimit, - }); - clearTimeout(timeout); - - set(state => { - const tree = {...state.tree}; - const newCollections = {...state.collections}; - - const items = data.result.map(c => { - return updateCollectionByReference( - newCollections, - c, - workspaceId + const jc = state.justCreated[pagerId] ?? false; + if (jc) { + Object.keys(jc as Record).forEach( + cId => { + if (!items.some(c => c.id === cId)) { + items.push(jc[cId]); + ++data.total; + } + } ); - }); + } - tree[workspaceId] = { - ...(tree[workspaceId] ?? {}), + tree[pagerId] = { + ...(tree[pagerId] ?? {}), items, total: data.total, expanding: false, @@ -218,36 +180,6 @@ export const useCollectionStore = create((set, getState) => ({ } }, - setRootCollections: workspaces => { - set(state => { - const newCollections = {...state.collections}; - const tree = {...state.tree}; - - workspaces.forEach(ws => { - const wsId = ws.id; - - const items = ws.collections.map(c => { - return updateCollectionByReference( - newCollections, - c, - ws.id - ); - }); - - tree[wsId] = { - items, - expanding: false, - loadingMore: false, - }; - }); - - return { - collections: newCollections, - tree, - }; - }); - }, - updateCollection: collection => { getState().partialUpdateCollection(collection.id, collection); }, @@ -312,7 +244,7 @@ export const useCollectionStore = create((set, getState) => ({ (oldPublic && false === updates.public) || (oldShared && false === updates.shared); if (shouldRefresh && state.tree[id]) { - state.loadChildren(oldColl.workspaceId, id); + state.load(oldColl.workspaceId, id); } return { @@ -325,6 +257,7 @@ export const useCollectionStore = create((set, getState) => ({ set(state => { const newCollections = {...state.collections}; const tree = {...state.tree}; + let justCreated = state.justCreated; const c = updateCollectionByReference( newCollections, @@ -340,7 +273,11 @@ export const useCollectionStore = create((set, getState) => ({ (parentCollection.children?.length ?? 0) === 0 ) { newCollections[parentCollection.id].children = [c]; + + justCreated = addJustCreated(justCreated, parentId, c, set); } + } else { + justCreated = addJustCreated(justCreated, workspaceId, c, set); } const pagerId = c.parentId ?? c.workspaceId; @@ -358,6 +295,7 @@ export const useCollectionStore = create((set, getState) => ({ return { tree, + justCreated, collections: newCollections, }; }); @@ -477,19 +415,79 @@ export function getNextPage(pager: CollectionPager): number | undefined { } } -const createPagerExpandingSetter = (pagerId: string) => { +const createPagerExpandingSetter = ( + pagerId: string, + parentId: string | undefined +) => { return (state: State) => { const tree = {...state.tree}; + const parentCollection = parentId + ? state.collections[parentId] + : undefined; + tree[pagerId] = { ...(tree[pagerId] ?? { loadingMore: false, - items: [], + items: parentCollection ? parentCollection.children : [], }), - ...tree[pagerId], expanding: true, }; return {tree}; }; }; + +function updateCollectionByReference( + state: Record, + collection: Collection | CollectionExtended, + workspaceId: string, + parentId?: string | undefined +): CollectionExtended { + const c = state[collection.id]; + + if (c) { + (Object.keys(collection) as (keyof typeof collection)[]).forEach(k => { + // @ts-expect-error key typing + c[k] = collection[k]; + }); + } else { + return (state[collection.id] = { + ...collection, + workspaceId, + parentId, + } as CollectionExtended); + } + + return c as CollectionExtended; +} + +function addJustCreated( + store: JustCreatedIndex, + parentId: string, + collection: CollectionExtended, + set: StoreApi['setState'] +): JustCreatedIndex { + const newStore = {...store}; + newStore[parentId] = { + ...(newStore[parentId] ?? {}), + }; + newStore[parentId][collection.id] = collection; + + setTimeout(() => { + set((prev: State) => { + const s = {...prev.justCreated}; + + if (s[parentId]) { + s[parentId] = { + ...(s[parentId] ?? {}), + }; + delete s[parentId][collection.id]; + } + + return s; + }); + }, 10000); + + return store; +} diff --git a/databox/client/src/store/workspaceStore.ts b/databox/client/src/store/workspaceStore.ts new file mode 100644 index 000000000..b9c5b6410 --- /dev/null +++ b/databox/client/src/store/workspaceStore.ts @@ -0,0 +1,73 @@ +import {create} from 'zustand'; +import {Workspace} from '../types'; +import {getWorkspaces} from '../api/workspace.ts'; + +type State = { + tree: Record; + workspaces: Workspace[]; + updateWorkspace: (workspace: Workspace) => void; + partialUpdateWorkspace: (id: string, updates: Partial) => void; + load: () => Promise; + loaded: boolean; + loading: boolean; + loadingMore: boolean; + total?: number; +}; + +export const useWorkspaceStore = create((set, getState) => ({ + workspaces: [], + tree: {}, + loading: false, + loaded: false, + loadingMore: false, + total: undefined, + + load: async (force?: boolean) => { + if (!force) { + const current = getState(); + if (current.loaded || current.loading) { + return; + } + } + + set({loading: true}); + try { + const data = await getWorkspaces(); + + set(state => { + const tree = {...state.tree}; + + data.result.forEach(w => { + tree[w.id] = w; + }); + + return { + tree, + workspaces: data.result, + loaded: true, + }; + }); + } finally { + set({loading: false}); + } + }, + + updateWorkspace: workspace => { + getState().partialUpdateWorkspace(workspace.id, workspace); + }, + + partialUpdateWorkspace: (id, updates) => { + set(state => { + const tree = {...state.tree}; + + tree[id] = { + ...(tree[id] ?? {}), + ...updates, + }; + + return { + tree, + }; + }); + }, +})); diff --git a/databox/client/src/types.ts b/databox/client/src/types.ts index f390c4adc..4e9342bb2 100644 --- a/databox/client/src/types.ts +++ b/databox/client/src/types.ts @@ -33,7 +33,7 @@ export type ShareAlternateUrl = { name: string; url: string; type: string | undefined; -} +}; export type Share = { id: string; @@ -275,7 +275,6 @@ export interface BasketAsset { export interface Workspace extends IPermissions { id: string; name: string; - collections: Collection[]; enabledLocales?: string[] | undefined; localeFallbacks?: string[] | undefined; createdAt: string; diff --git a/lib/js/phrasea-ui/src/components/FullPageLoader.tsx b/lib/js/phrasea-ui/src/components/FullPageLoader.tsx index 157837346..10a29e7cb 100644 --- a/lib/js/phrasea-ui/src/components/FullPageLoader.tsx +++ b/lib/js/phrasea-ui/src/components/FullPageLoader.tsx @@ -1,22 +1,34 @@ -import {Backdrop, CircularProgress} from "@mui/material"; +import {Backdrop, CircularProgress, Typography} from "@mui/material"; +import {ReactNode} from "react"; type Props = { open?: boolean; backdrop?: boolean; + message?: ReactNode; }; export default function FullPageLoader({ open = true, backdrop = true, + message, }: Props) { return theme.zIndex.drawer + 1 - }} + sx={theme => ({ + color: backdrop ? theme.palette.common.white : undefined, + zIndex: theme.zIndex.drawer + 1, + flexDirection: 'column', + })} open={open} invisible={!backdrop} > - +
+ +
+ {message ? {message} : null}
} diff --git a/lib/js/react-ps/src/hooks/useEffectOnce.tsx b/lib/js/react-ps/src/hooks/useEffectOnce.tsx deleted file mode 100644 index b5045edff..000000000 --- a/lib/js/react-ps/src/hooks/useEffectOnce.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import {useEffect, useRef} from "react"; - -function propsAreSame(a: any[], b: any[]): boolean { - for (let i in a) { - if (b[i] !== a[i]) { - return false; - } - } - - return true; -} - -export default function useEffectOnce( - handler: () => void, - trackingValues: any[], -) { - const runRef = useRef(false); - const trackingRef = useRef(trackingValues); - - useEffect(() => { - if (!runRef.current || !propsAreSame(trackingRef.current, trackingValues)) { - runRef.current = true; - trackingRef.current = trackingValues; - - handler(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, trackingValues); -} diff --git a/lib/js/react-ps/src/index.ts b/lib/js/react-ps/src/index.ts index 25f4bd9e4..93c4c8d89 100644 --- a/lib/js/react-ps/src/index.ts +++ b/lib/js/react-ps/src/index.ts @@ -1,7 +1,5 @@ import DashboardMenu from "./components/DashboardMenu/DashboardMenu"; -import useEffectOnce from "./hooks/useEffectOnce"; export { DashboardMenu, - useEffectOnce, }; diff --git a/lib/php/admin-bundle/Controller/AbstractAdminCrudController.php b/lib/php/admin-bundle/Controller/AbstractAdminCrudController.php index b11dc6ba3..e45dcf6e5 100644 --- a/lib/php/admin-bundle/Controller/AbstractAdminCrudController.php +++ b/lib/php/admin-bundle/Controller/AbstractAdminCrudController.php @@ -7,7 +7,10 @@ use EasyCorp\Bundle\EasyAdminBundle\Config\Action; use EasyCorp\Bundle\EasyAdminBundle\Config\Actions; use EasyCorp\Bundle\EasyAdminBundle\Config\Crud; +use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext; use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; +use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator; +use Symfony\Component\HttpFoundation\RedirectResponse; abstract class AbstractAdminCrudController extends AbstractCrudController { @@ -26,4 +29,10 @@ public function configureActions(Actions $actions): Actions return $actions ->add(Crud::PAGE_INDEX, Action::DETAIL); } + + protected function returnToReferer(AdminContext $context): RedirectResponse + { + return $this->redirect($context->getReferrer() + ?? $this->container->get(AdminUrlGenerator::class)->setAction(Action::INDEX)->generateUrl()); + } } diff --git a/lib/php/admin-bundle/Field/YamlField.php b/lib/php/admin-bundle/Field/YamlField.php index 3e67c7522..e0a3d1c39 100644 --- a/lib/php/admin-bundle/Field/YamlField.php +++ b/lib/php/admin-bundle/Field/YamlField.php @@ -3,8 +3,8 @@ namespace Alchemy\AdminBundle\Field; use Alchemy\AdminBundle\Form\YamlType; -use EasyCorp\Bundle\EasyAdminBundle\Field\FieldTrait; use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldInterface; +use EasyCorp\Bundle\EasyAdminBundle\Field\FieldTrait; final class YamlField implements FieldInterface { diff --git a/lib/php/admin-bundle/Form/JsonType.php b/lib/php/admin-bundle/Form/JsonType.php index 74c5a3d1a..753908859 100644 --- a/lib/php/admin-bundle/Form/JsonType.php +++ b/lib/php/admin-bundle/Form/JsonType.php @@ -26,8 +26,8 @@ public function transform(mixed $value) public function reverseTransform(mixed $value) { - if ($value !== null && json_validate($value) === false) { - return ['input-error' => 'Invalid JSON: ' . json_last_error_msg()]; + if (null !== $value && false === json_validate($value)) { + return ['input-error' => 'Invalid JSON: '.json_last_error_msg()]; } return json_decode($value, true, 512, JSON_THROW_ON_ERROR); @@ -36,7 +36,7 @@ public function reverseTransform(mixed $value) public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults([ - 'attr'=> [ + 'attr' => [ 'rows' => 10, 'style' => 'font-family: "Courier New"', ], @@ -48,9 +48,9 @@ function (mixed $data, ExecutionContextInterface $context) { ->buildViolation($data['input-error']) ->addViolation(); } - } - ) - ] + } + ), + ], ]); } diff --git a/lib/php/admin-bundle/Form/YamlType.php b/lib/php/admin-bundle/Form/YamlType.php index 19690cb7b..38ccee929 100644 --- a/lib/php/admin-bundle/Form/YamlType.php +++ b/lib/php/admin-bundle/Form/YamlType.php @@ -4,20 +4,20 @@ namespace Alchemy\AdminBundle\Form; -use Symfony\Component\Yaml\Yaml; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Yaml\Exception\ParseException; +use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Validator\Constraints as Assert; -use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Yaml\Exception\ParseException; +use Symfony\Component\Yaml\Yaml; -class YamlType extends AbstractType +class YamlType extends AbstractType { public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults([ - 'attr'=> [ + 'attr' => [ 'rows' => 10, 'style' => 'font-family: "Courier New"', ], @@ -31,9 +31,9 @@ function (mixed $data, ExecutionContextInterface $context) { ->buildViolation(sprintf('YAML error: %s', $e->getMessage())) ->addViolation(); } - } - ) - ] + } + ), + ], ]); } diff --git a/lib/php/messenger-bundle/DependencyInjection/AlchemyMessengerExtension.php b/lib/php/messenger-bundle/DependencyInjection/AlchemyMessengerExtension.php index a0f35400f..b7f42b380 100644 --- a/lib/php/messenger-bundle/DependencyInjection/AlchemyMessengerExtension.php +++ b/lib/php/messenger-bundle/DependencyInjection/AlchemyMessengerExtension.php @@ -24,6 +24,11 @@ public function load(array $configs, ContainerBuilder $container): void $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('services.yaml'); + + $bundles = $container->getParameter('kernel.bundles'); + if (isset($bundles['SentryBundle'])) { + $loader->load('sentry.yaml'); + } } public function prepend(ContainerBuilder $container): void diff --git a/lib/php/messenger-bundle/Listener/SentryMessengerListener.php b/lib/php/messenger-bundle/Listener/SentryMessengerListener.php new file mode 100644 index 000000000..991eacd73 --- /dev/null +++ b/lib/php/messenger-bundle/Listener/SentryMessengerListener.php @@ -0,0 +1,124 @@ +captureSoftFails && $event->willRetry()) { + return; + } + + $this->hub->withScope(function (Scope $scope) use ($event): void { + $envelope = $event->getEnvelope(); + $exception = $event->getThrowable(); + + $scope->setTag('messenger.receiver_name', $event->getReceiverName()); + $scope->setTag('messenger.message_class', \get_class($envelope->getMessage())); + + /** @var BusNameStamp|null $messageBusStamp */ + $messageBusStamp = $envelope->last(BusNameStamp::class); + + if (null !== $messageBusStamp) { + $scope->setTag('messenger.message_bus', $messageBusStamp->getBusName()); + } + + $scope->setExtras([ + 'Messenger Message' => get_debug_type($envelope->getMessage()), + 'Messenger Payload' => $this->serializer->serialize( + $envelope->getMessage(), + JsonEncoder::FORMAT, [ + JsonEncode::OPTIONS => JSON_PRETTY_PRINT + ] + ), + ]); + + $this->captureException($exception, $event->willRetry()); + }); + + $this->flushClient(); + } + + /** + * This method is called for each handled message. + * + * @param WorkerMessageHandledEvent $event The event + */ + public function handleWorkerMessageHandledEvent(WorkerMessageHandledEvent $event): void + { + // Flush normally happens at shutdown... which only happens in the worker if it is run with a lifecycle limit + // such as --time=X or --limit=Y. Flush immediately in a background worker. + $this->flushClient(); + } + + /** + * Creates Sentry events from the given exception. + * + * Unpacks multiple exceptions wrapped in a HandlerFailedException and notifies + * Sentry of each individual exception. + * + * If the message will be retried the exceptions will be marked as handled + * in Sentry. + */ + private function captureException(\Throwable $exception, bool $willRetry): void + { + if ($exception instanceof WrappedExceptionsInterface) { + $exception = $exception->getWrappedExceptions(); + } elseif ($exception instanceof HandlerFailedException && method_exists($exception, 'getNestedExceptions')) { + $exception = $exception->getNestedExceptions(); + } elseif ($exception instanceof DelayedMessageHandlingException && method_exists($exception, 'getExceptions')) { + $exception = $exception->getExceptions(); + } + + if (\is_array($exception)) { + foreach ($exception as $nestedException) { + $this->captureException($nestedException, $willRetry); + } + + return; + } + + $hint = EventHint::fromArray([ + 'exception' => $exception, + 'mechanism' => new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, $willRetry), + ]); + + $this->hub->captureEvent(Event::createEvent(), $hint); + } + + private function flushClient(): void + { + $client = $this->hub->getClient(); + + if (null !== $client) { + $client->flush(); + } + } +} diff --git a/lib/php/messenger-bundle/Resources/config/sentry.yaml b/lib/php/messenger-bundle/Resources/config/sentry.yaml new file mode 100644 index 000000000..66511e246 --- /dev/null +++ b/lib/php/messenger-bundle/Resources/config/sentry.yaml @@ -0,0 +1,6 @@ +services: + _defaults: + autowire: true + autoconfigure: true + + Alchemy\MessengerBundle\Listener\SentryMessengerListener: ~ diff --git a/lib/php/messenger-bundle/composer.json b/lib/php/messenger-bundle/composer.json index b1dff1e23..c28ac5c9d 100644 --- a/lib/php/messenger-bundle/composer.json +++ b/lib/php/messenger-bundle/composer.json @@ -10,6 +10,7 @@ "require": { "php": "^8.3", "symfony/framework-bundle": "^6.4", + "symfony/serializer": "^6.4", "symfony/messenger": "^6.4", "symfony/doctrine-messenger": "^6.4.7" }, diff --git a/lib/php/messenger-bundle/composer.lock b/lib/php/messenger-bundle/composer.lock index 878b6132e..b5c4aa225 100644 --- a/lib/php/messenger-bundle/composer.lock +++ b/lib/php/messenger-bundle/composer.lock @@ -4,8 +4,163 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9c16b64cf432ddbdcd1854a1f848c8a1", + "content-hash": "2df8d40377d29c23c89a5a1b08155317", "packages": [ + { + "name": "doctrine/dbal", + "version": "4.2.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "dadd35300837a3a2184bd47d403333b15d0a9bd0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/dadd35300837a3a2184bd47d403333b15d0a9bd0", + "reference": "dadd35300837a3a2184bd47d403333b15d0a9bd0", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^0.5.3|^1", + "php": "^8.1", + "psr/cache": "^1|^2|^3", + "psr/log": "^1|^2|^3" + }, + "require-dev": { + "doctrine/coding-standard": "12.0.0", + "fig/log-test": "^1", + "jetbrains/phpstorm-stubs": "2023.2", + "phpstan/phpstan": "1.12.6", + "phpstan/phpstan-phpunit": "1.4.0", + "phpstan/phpstan-strict-rules": "^1.6", + "phpunit/phpunit": "10.5.30", + "psalm/plugin-phpunit": "0.19.0", + "slevomat/coding-standard": "8.13.1", + "squizlabs/php_codesniffer": "3.10.2", + "symfony/cache": "^6.3.8|^7.0", + "symfony/console": "^5.4|^6.3|^7.0", + "vimeo/psalm": "5.25.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/4.2.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "time": "2024-10-10T18:01:27+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", + "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9", + "phpstan/phpstan": "1.4.10 || 1.10.15", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psalm/plugin-phpunit": "0.18.4", + "psr/log": "^1 || ^2 || ^3", + "vimeo/psalm": "4.30.0 || 5.12.0" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.3" + }, + "time": "2024-01-30T19:34:25+00:00" + }, { "name": "psr/cache", "version": "3.0.0", @@ -208,16 +363,16 @@ }, { "name": "psr/log", - "version": "3.0.0", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", "shasum": "" }, "require": { @@ -252,22 +407,22 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/3.0.0" + "source": "https://github.com/php-fig/log/tree/3.0.2" }, - "time": "2021-07-14T16:46:02+00:00" + "time": "2024-09-11T13:17:53+00:00" }, { "name": "symfony/cache", - "version": "v7.0.6", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "2d0d3f92c74c445410d05374908b03e0a1131e2b" + "reference": "86e5296b10e4dec8c8441056ca606aedb8a3be0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/2d0d3f92c74c445410d05374908b03e0a1131e2b", - "reference": "2d0d3f92c74c445410d05374908b03e0a1131e2b", + "url": "https://api.github.com/repos/symfony/cache/zipball/86e5296b10e4dec8c8441056ca606aedb8a3be0a", + "reference": "86e5296b10e4dec8c8441056ca606aedb8a3be0a", "shasum": "" }, "require": { @@ -275,6 +430,7 @@ "psr/cache": "^2.0|^3.0", "psr/log": "^1.1|^2|^3", "symfony/cache-contracts": "^2.5|^3", + "symfony/deprecation-contracts": "^2.5|^3.0", "symfony/service-contracts": "^2.5|^3", "symfony/var-exporter": "^6.4|^7.0" }, @@ -334,7 +490,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v7.0.6" + "source": "https://github.com/symfony/cache/tree/v7.1.5" }, "funding": [ { @@ -350,20 +506,20 @@ "type": "tidelift" } ], - "time": "2024-03-27T19:55:25+00:00" + "time": "2024-09-17T09:16:35+00:00" }, { "name": "symfony/cache-contracts", - "version": "v3.4.2", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/cache-contracts.git", - "reference": "2c9db6509a1b21dad229606897639d3284f54b2a" + "reference": "df6a1a44c890faded49a5fca33c2d5c5fd3c2197" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/2c9db6509a1b21dad229606897639d3284f54b2a", - "reference": "2c9db6509a1b21dad229606897639d3284f54b2a", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/df6a1a44c890faded49a5fca33c2d5c5fd3c2197", + "reference": "df6a1a44c890faded49a5fca33c2d5c5fd3c2197", "shasum": "" }, "require": { @@ -373,7 +529,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -410,7 +566,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/cache-contracts/tree/v3.4.2" + "source": "https://github.com/symfony/cache-contracts/tree/v3.5.0" }, "funding": [ { @@ -426,20 +582,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/clock", - "version": "v7.0.5", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/clock.git", - "reference": "8b9d08887353d627d5f6c3bf3373b398b49051c2" + "reference": "3dfc8b084853586de51dd1441c6242c76a28cbe7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/clock/zipball/8b9d08887353d627d5f6c3bf3373b398b49051c2", - "reference": "8b9d08887353d627d5f6c3bf3373b398b49051c2", + "url": "https://api.github.com/repos/symfony/clock/zipball/3dfc8b084853586de51dd1441c6242c76a28cbe7", + "reference": "3dfc8b084853586de51dd1441c6242c76a28cbe7", "shasum": "" }, "require": { @@ -484,7 +640,7 @@ "time" ], "support": { - "source": "https://github.com/symfony/clock/tree/v7.0.5" + "source": "https://github.com/symfony/clock/tree/v7.1.1" }, "funding": [ { @@ -500,26 +656,26 @@ "type": "tidelift" } ], - "time": "2024-03-02T12:46:12+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/config", - "version": "v7.0.6", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "7fc7e18a73ec8125fd95928c0340470d64760deb" + "reference": "2210fc99fa42a259eb6c89d1f724ce0c4d62d5d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/7fc7e18a73ec8125fd95928c0340470d64760deb", - "reference": "7fc7e18a73ec8125fd95928c0340470d64760deb", + "url": "https://api.github.com/repos/symfony/config/zipball/2210fc99fa42a259eb6c89d1f724ce0c4d62d5d2", + "reference": "2210fc99fa42a259eb6c89d1f724ce0c4d62d5d2", "shasum": "" }, "require": { "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/filesystem": "^6.4|^7.0", + "symfony/filesystem": "^7.1", "symfony/polyfill-ctype": "~1.8" }, "conflict": { @@ -559,7 +715,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v7.0.6" + "source": "https://github.com/symfony/config/tree/v7.1.1" }, "funding": [ { @@ -575,27 +731,27 @@ "type": "tidelift" } ], - "time": "2024-03-27T19:55:25+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/dependency-injection", - "version": "v7.0.6", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "ff57b5c7d518c39eeb4e69dc0d1ec70723a117b9" + "reference": "38465f925ec4e0707b090e9147c65869837d639d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/ff57b5c7d518c39eeb4e69dc0d1ec70723a117b9", - "reference": "ff57b5c7d518c39eeb4e69dc0d1ec70723a117b9", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/38465f925ec4e0707b090e9147c65869837d639d", + "reference": "38465f925ec4e0707b090e9147c65869837d639d", "shasum": "" }, "require": { "php": ">=8.2", "psr/container": "^1.1|^2.0", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/service-contracts": "^3.3", + "symfony/service-contracts": "^3.5", "symfony/var-exporter": "^6.4|^7.0" }, "conflict": { @@ -639,7 +795,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v7.0.6" + "source": "https://github.com/symfony/dependency-injection/tree/v7.1.5" }, "funding": [ { @@ -655,20 +811,20 @@ "type": "tidelift" } ], - "time": "2024-03-28T09:20:36+00:00" + "time": "2024-09-20T08:28:38+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.4.0", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", - "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", "shasum": "" }, "require": { @@ -677,7 +833,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -706,7 +862,79 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:32:20+00:00" + }, + { + "name": "symfony/doctrine-messenger", + "version": "v6.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/doctrine-messenger.git", + "reference": "68ae3eeb7ee515d77fe6d0164c8df42c2ebad513" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/doctrine-messenger/zipball/68ae3eeb7ee515d77fe6d0164c8df42c2ebad513", + "reference": "68ae3eeb7ee515d77fe6d0164c8df42c2ebad513", + "shasum": "" + }, + "require": { + "doctrine/dbal": "^2.13|^3|^4", + "php": ">=8.1", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/persistence": "<1.3" + }, + "require-dev": { + "doctrine/persistence": "^1.3|^2|^3", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/serializer": "^5.4|^6.0|^7.0" + }, + "type": "symfony-messenger-bridge", + "autoload": { + "psr-4": { + "Symfony\\Component\\Messenger\\Bridge\\Doctrine\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Doctrine Messenger Bridge", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/doctrine-messenger/tree/v6.4.12" }, "funding": [ { @@ -722,20 +950,20 @@ "type": "tidelift" } ], - "time": "2023-05-23T14:45:45+00:00" + "time": "2024-09-20T08:15:52+00:00" }, { "name": "symfony/error-handler", - "version": "v7.0.6", + "version": "v7.1.3", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "46a4cc138f799886d4bd70477c55c699d3e9dfc8" + "reference": "432bb369952795c61ca1def65e078c4a80dad13c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/46a4cc138f799886d4bd70477c55c699d3e9dfc8", - "reference": "46a4cc138f799886d4bd70477c55c699d3e9dfc8", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/432bb369952795c61ca1def65e078c4a80dad13c", + "reference": "432bb369952795c61ca1def65e078c4a80dad13c", "shasum": "" }, "require": { @@ -781,7 +1009,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.0.6" + "source": "https://github.com/symfony/error-handler/tree/v7.1.3" }, "funding": [ { @@ -797,20 +1025,20 @@ "type": "tidelift" } ], - "time": "2024-03-19T11:57:22+00:00" + "time": "2024-07-26T13:02:51+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.0.3", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "834c28d533dd0636f910909d01b9ff45cc094b5e" + "reference": "9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/834c28d533dd0636f910909d01b9ff45cc094b5e", - "reference": "834c28d533dd0636f910909d01b9ff45cc094b5e", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7", + "reference": "9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7", "shasum": "" }, "require": { @@ -861,7 +1089,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.0.3" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.1.1" }, "funding": [ { @@ -877,20 +1105,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T15:02:46+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.4.2", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "4e64b49bf370ade88e567de29465762e316e4224" + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/4e64b49bf370ade88e567de29465762e316e4224", - "reference": "4e64b49bf370ade88e567de29465762e316e4224", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/8f93aec25d41b72493c6ddff14e916177c9efc50", + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50", "shasum": "" }, "require": { @@ -900,7 +1128,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -937,7 +1165,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.4.2" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.0" }, "funding": [ { @@ -953,20 +1181,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/filesystem", - "version": "v7.0.6", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "408105dff4c104454100730bdfd1a9cdd993f04d" + "reference": "61fe0566189bf32e8cfee78335d8776f64a66f5a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/408105dff4c104454100730bdfd1a9cdd993f04d", - "reference": "408105dff4c104454100730bdfd1a9cdd993f04d", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/61fe0566189bf32e8cfee78335d8776f64a66f5a", + "reference": "61fe0566189bf32e8cfee78335d8776f64a66f5a", "shasum": "" }, "require": { @@ -974,6 +1202,9 @@ "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.8" }, + "require-dev": { + "symfony/process": "^6.4|^7.0" + }, "type": "library", "autoload": { "psr-4": { @@ -1000,7 +1231,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.0.6" + "source": "https://github.com/symfony/filesystem/tree/v7.1.5" }, "funding": [ { @@ -1016,20 +1247,20 @@ "type": "tidelift" } ], - "time": "2024-03-21T19:37:36+00:00" + "time": "2024-09-17T09:16:35+00:00" }, { "name": "symfony/finder", - "version": "v7.0.0", + "version": "v7.1.4", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "6e5688d69f7cfc4ed4a511e96007e06c2d34ce56" + "reference": "d95bbf319f7d052082fb7af147e0f835a695e823" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/6e5688d69f7cfc4ed4a511e96007e06c2d34ce56", - "reference": "6e5688d69f7cfc4ed4a511e96007e06c2d34ce56", + "url": "https://api.github.com/repos/symfony/finder/zipball/d95bbf319f7d052082fb7af147e0f835a695e823", + "reference": "d95bbf319f7d052082fb7af147e0f835a695e823", "shasum": "" }, "require": { @@ -1064,7 +1295,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.0.0" + "source": "https://github.com/symfony/finder/tree/v7.1.4" }, "funding": [ { @@ -1080,20 +1311,20 @@ "type": "tidelift" } ], - "time": "2023-10-31T17:59:56+00:00" + "time": "2024-08-13T14:28:19+00:00" }, { "name": "symfony/framework-bundle", - "version": "v6.4.6", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "49093e57c7eea2ecd1603b0218c797fc37514ae9" + "reference": "6a9665bd1fae37b198429775c6132f193339434f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/49093e57c7eea2ecd1603b0218c797fc37514ae9", - "reference": "49093e57c7eea2ecd1603b0218c797fc37514ae9", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/6a9665bd1fae37b198429775c6132f193339434f", + "reference": "6a9665bd1fae37b198429775c6132f193339434f", "shasum": "" }, "require": { @@ -1102,7 +1333,7 @@ "php": ">=8.1", "symfony/cache": "^5.4|^6.0|^7.0", "symfony/config": "^6.1|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4.12|^7.0", "symfony/deprecation-contracts": "^2.5|^3", "symfony/error-handler": "^6.1|^7.0", "symfony/event-dispatcher": "^5.4|^6.0|^7.0", @@ -1212,7 +1443,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v6.4.6" + "source": "https://github.com/symfony/framework-bundle/tree/v6.4.12" }, "funding": [ { @@ -1228,20 +1459,20 @@ "type": "tidelift" } ], - "time": "2024-03-23T16:06:09+00:00" + "time": "2024-09-20T13:34:56+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.0.6", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "8789625dcf36e5fbf753014678a1e090f1bc759c" + "reference": "e30ef73b1e44eea7eb37ba69600a354e553f694b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/8789625dcf36e5fbf753014678a1e090f1bc759c", - "reference": "8789625dcf36e5fbf753014678a1e090f1bc759c", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e30ef73b1e44eea7eb37ba69600a354e553f694b", + "reference": "e30ef73b1e44eea7eb37ba69600a354e553f694b", "shasum": "" }, "require": { @@ -1289,7 +1520,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.0.6" + "source": "https://github.com/symfony/http-foundation/tree/v7.1.5" }, "funding": [ { @@ -1305,20 +1536,20 @@ "type": "tidelift" } ], - "time": "2024-03-19T11:46:48+00:00" + "time": "2024-09-20T08:28:38+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.4.6", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "060038863743fd0cd982be06acecccf246d35653" + "reference": "96df83d51b5f78804f70c093b97310794fd6257b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/060038863743fd0cd982be06acecccf246d35653", - "reference": "060038863743fd0cd982be06acecccf246d35653", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/96df83d51b5f78804f70c093b97310794fd6257b", + "reference": "96df83d51b5f78804f70c093b97310794fd6257b", "shasum": "" }, "require": { @@ -1373,6 +1604,7 @@ "symfony/translation-contracts": "^2.5|^3", "symfony/uid": "^5.4|^6.0|^7.0", "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^5.4|^6.4|^7.0", "symfony/var-exporter": "^6.2|^7.0", "twig/twig": "^2.13|^3.0.4" }, @@ -1402,7 +1634,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.4.6" + "source": "https://github.com/symfony/http-kernel/tree/v6.4.12" }, "funding": [ { @@ -1418,20 +1650,20 @@ "type": "tidelift" } ], - "time": "2024-04-03T06:09:15+00:00" + "time": "2024-09-21T06:02:57+00:00" }, { "name": "symfony/messenger", - "version": "v6.4.6", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/messenger.git", - "reference": "4b7f073d341f6d0b431a1c643b40aa21506ca820" + "reference": "05035355ef94de2cb054f8697e65d82f67bf89d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/messenger/zipball/4b7f073d341f6d0b431a1c643b40aa21506ca820", - "reference": "4b7f073d341f6d0b431a1c643b40aa21506ca820", + "url": "https://api.github.com/repos/symfony/messenger/zipball/05035355ef94de2cb054f8697e65d82f67bf89d4", + "reference": "05035355ef94de2cb054f8697e65d82f67bf89d4", "shasum": "" }, "require": { @@ -1489,7 +1721,7 @@ "description": "Helps applications send and receive messages to/from other applications or via message queues", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/messenger/tree/v6.4.6" + "source": "https://github.com/symfony/messenger/tree/v6.4.12" }, "funding": [ { @@ -1505,24 +1737,24 @@ "type": "tidelift" } ], - "time": "2024-03-19T11:56:30+00:00" + "time": "2024-09-08T12:31:10+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.29.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-ctype": "*" @@ -1568,7 +1800,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" }, "funding": [ { @@ -1584,24 +1816,24 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.29.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-mbstring": "*" @@ -1648,7 +1880,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" }, "funding": [ { @@ -1664,24 +1896,24 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/polyfill-php80", - "version": "v1.29.0", + "name": "symfony/polyfill-php83", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { @@ -1695,7 +1927,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" + "Symfony\\Polyfill\\Php83\\": "" }, "classmap": [ "Resources/stubs" @@ -1706,10 +1938,6 @@ "MIT" ], "authors": [ - { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -1719,7 +1947,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -1728,7 +1956,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0" }, "funding": [ { @@ -1744,42 +1972,46 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/polyfill-php83", - "version": "v1.29.0", + "name": "symfony/routing", + "version": "v7.1.4", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "86fcae159633351e5fd145d1c47de6c528f8caff" + "url": "https://github.com/symfony/routing.git", + "reference": "1500aee0094a3ce1c92626ed8cf3c2037e86f5a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/86fcae159633351e5fd145d1c47de6c528f8caff", - "reference": "86fcae159633351e5fd145d1c47de6c528f8caff", + "url": "https://api.github.com/repos/symfony/routing/zipball/1500aee0094a3ce1c92626ed8cf3c2037e86f5a7", + "reference": "1500aee0094a3ce1c92626ed8cf3c2037e86f5a7", "shasum": "" }, "require": { - "php": ">=7.1", - "symfony/polyfill-php80": "^1.14" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" }, - "type": "library", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } + "conflict": { + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0" }, + "type": "library", "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Php83\\": "" + "Symfony\\Component\\Routing\\": "" }, - "classmap": [ - "Resources/stubs" + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -1788,24 +2020,24 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "description": "Maps an HTTP request to a set of configuration variables", "homepage": "https://symfony.com", "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" + "router", + "routing", + "uri", + "url" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.29.0" + "source": "https://github.com/symfony/routing/tree/v7.1.4" }, "funding": [ { @@ -1821,43 +2053,66 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-08-29T08:16:25+00:00" }, { - "name": "symfony/routing", - "version": "v7.0.6", + "name": "symfony/serializer", + "version": "v6.4.12", "source": { "type": "git", - "url": "https://github.com/symfony/routing.git", - "reference": "cded64e5bbf9f31786f1055fcc76718fdd77519c" + "url": "https://github.com/symfony/serializer.git", + "reference": "10ae9c1b90f4809ccb7277cc8fe8d80b3af4412c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/cded64e5bbf9f31786f1055fcc76718fdd77519c", - "reference": "cded64e5bbf9f31786f1055fcc76718fdd77519c", + "url": "https://api.github.com/repos/symfony/serializer/zipball/10ae9c1b90f4809ccb7277cc8fe8d80b3af4412c", + "reference": "10ae9c1b90f4809ccb7277cc8fe8d80b3af4412c", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3" + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8" }, "conflict": { - "symfony/config": "<6.4", - "symfony/dependency-injection": "<6.4", - "symfony/yaml": "<6.4" + "doctrine/annotations": "<1.12", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/dependency-injection": "<5.4", + "symfony/property-access": "<5.4", + "symfony/property-info": "<5.4.24|>=6,<6.2.11", + "symfony/uid": "<5.4", + "symfony/validator": "<6.4", + "symfony/yaml": "<5.4" }, "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0" + "doctrine/annotations": "^1.12|^2", + "phpdocumentor/reflection-docblock": "^3.2|^4.0|^5.0", + "seld/jsonlint": "^1.10", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/filesystem": "^5.4|^6.0|^7.0", + "symfony/form": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/property-access": "^5.4.26|^6.3|^7.0", + "symfony/property-info": "^5.4.24|^6.2.11|^7.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^5.4|^6.0|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0", + "symfony/var-exporter": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Routing\\": "" + "Symfony\\Component\\Serializer\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -1877,16 +2132,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Maps an HTTP request to a set of configuration variables", + "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", - "keywords": [ - "router", - "routing", - "uri", - "url" - ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.0.6" + "source": "https://github.com/symfony/serializer/tree/v6.4.12" }, "funding": [ { @@ -1902,25 +2151,26 @@ "type": "tidelift" } ], - "time": "2024-03-28T21:02:11+00:00" + "time": "2024-09-20T08:15:52+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.4.2", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "11bbf19a0fb7b36345861e85c5768844c552906e" + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/11bbf19a0fb7b36345861e85c5768844c552906e", - "reference": "11bbf19a0fb7b36345861e85c5768844c552906e", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", "shasum": "" }, "require": { "php": ">=8.1", - "psr/container": "^1.1|^2.0" + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { "ext-psr": "<1.1|>=2" @@ -1928,7 +2178,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -1968,7 +2218,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.4.2" + "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" }, "funding": [ { @@ -1984,20 +2234,20 @@ "type": "tidelift" } ], - "time": "2023-12-19T21:51:00+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.0.6", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "66d13dc207d5dab6b4f4c2b5460efe1bea29dbfb" + "reference": "e20e03889539fd4e4211e14d2179226c513c010d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/66d13dc207d5dab6b4f4c2b5460efe1bea29dbfb", - "reference": "66d13dc207d5dab6b4f4c2b5460efe1bea29dbfb", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/e20e03889539fd4e4211e14d2179226c513c010d", + "reference": "e20e03889539fd4e4211e14d2179226c513c010d", "shasum": "" }, "require": { @@ -2051,7 +2301,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.0.6" + "source": "https://github.com/symfony/var-dumper/tree/v7.1.5" }, "funding": [ { @@ -2067,20 +2317,20 @@ "type": "tidelift" } ], - "time": "2024-03-19T11:57:22+00:00" + "time": "2024-09-16T10:07:02+00:00" }, { "name": "symfony/var-exporter", - "version": "v7.0.6", + "version": "v7.1.2", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "c74c568d2a15a1d407cf40d61ea82bc2d521e27b" + "reference": "b80a669a2264609f07f1667f891dbfca25eba44c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/c74c568d2a15a1d407cf40d61ea82bc2d521e27b", - "reference": "c74c568d2a15a1d407cf40d61ea82bc2d521e27b", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/b80a669a2264609f07f1667f891dbfca25eba44c", + "reference": "b80a669a2264609f07f1667f891dbfca25eba44c", "shasum": "" }, "require": { @@ -2127,7 +2377,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.0.6" + "source": "https://github.com/symfony/var-exporter/tree/v7.1.2" }, "funding": [ { @@ -2143,41 +2393,36 @@ "type": "tidelift" } ], - "time": "2024-03-20T21:25:22+00:00" + "time": "2024-06-28T08:00:31+00:00" } ], "packages-dev": [ { - "name": "composer/pcre", - "version": "3.1.3", + "name": "clue/ndjson-react", + "version": "v1.3.0", "source": { "type": "git", - "url": "https://github.com/composer/pcre.git", - "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8" + "url": "https://github.com/clue/reactphp-ndjson.git", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", - "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", + "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0" + "php": ">=5.3", + "react/stream": "^1.2" }, "require-dev": { - "phpstan/phpstan": "^1.3", - "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^5" + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.2" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.x-dev" - } - }, "autoload": { "psr-4": { - "Composer\\Pcre\\": "src" + "Clue\\React\\NDJson\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2186,21 +2431,98 @@ ], "authors": [ { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" + "name": "Christian Lück", + "email": "christian@clue.engineering" } ], - "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", + "homepage": "https://github.com/clue/reactphp-ndjson", "keywords": [ - "PCRE", - "preg", + "NDJSON", + "json", + "jsonlines", + "newline", + "reactphp", + "streaming" + ], + "support": { + "issues": "https://github.com/clue/reactphp-ndjson/issues", + "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-12-23T10:58:28+00:00" + }, + { + "name": "composer/pcre", + "version": "3.3.1", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/63aaeac21d7e775ff9bc9d45021e1745c97521c4", + "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.11.10", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", "regex", "regular expression" ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.1.3" + "source": "https://github.com/composer/pcre/tree/3.3.1" }, "funding": [ { @@ -2216,28 +2538,28 @@ "type": "tidelift" } ], - "time": "2024-03-19T10:26:25+00:00" + "time": "2024-08-27T18:44:43+00:00" }, { "name": "composer/semver", - "version": "3.4.0", + "version": "3.4.3", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32" + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32", - "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32", + "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", "shasum": "" }, "require": { "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^1.4", - "symfony/phpunit-bridge": "^4.2 || ^5" + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" }, "type": "library", "extra": { @@ -2281,7 +2603,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.0" + "source": "https://github.com/composer/semver/tree/3.4.3" }, "funding": [ { @@ -2297,20 +2619,20 @@ "type": "tidelift" } ], - "time": "2023-08-31T09:50:34+00:00" + "time": "2024-09-19T14:15:21+00:00" }, { "name": "composer/xdebug-handler", - "version": "3.0.4", + "version": "3.0.5", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "4f988f8fdf580d53bdb2d1278fe93d1ed5462255" + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/4f988f8fdf580d53bdb2d1278fe93d1ed5462255", - "reference": "4f988f8fdf580d53bdb2d1278fe93d1ed5462255", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", "shasum": "" }, "require": { @@ -2347,7 +2669,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/3.0.4" + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" }, "funding": [ { @@ -2363,29 +2685,144 @@ "type": "tidelift" } ], - "time": "2024-03-26T18:29:49+00:00" + "time": "2024-05-06T16:37:16+00:00" + }, + { + "name": "evenement/evenement", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evenement\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "8520451a140d3f46ac33042715115e290cf5785f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", + "reference": "8520451a140d3f46ac33042715115e290cf5785f", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^1.9.2", + "phpstan/phpstan-deprecation-rules": "^1.0.0", + "phpstan/phpstan-phpunit": "^1.2.2", + "phpstan/phpstan-strict-rules": "^1.4.4", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2024-08-06T10:04:20+00:00" }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.54.0", + "version": "v3.64.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "2aecbc8640d7906c38777b3dcab6f4ca79004d08" + "reference": "58dd9c931c785a79739310aef5178928305ffa67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/2aecbc8640d7906c38777b3dcab6f4ca79004d08", - "reference": "2aecbc8640d7906c38777b3dcab6f4ca79004d08", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/58dd9c931c785a79739310aef5178928305ffa67", + "reference": "58dd9c931c785a79739310aef5178928305ffa67", "shasum": "" }, "require": { + "clue/ndjson-react": "^1.0", "composer/semver": "^3.4", "composer/xdebug-handler": "^3.0.3", "ext-filter": "*", "ext-json": "*", "ext-tokenizer": "*", + "fidry/cpu-core-counter": "^1.0", "php": "^7.4 || ^8.0", + "react/child-process": "^0.6.5", + "react/event-loop": "^1.0", + "react/promise": "^2.0 || ^3.0", + "react/socket": "^1.0", + "react/stream": "^1.0", "sebastian/diff": "^4.0 || ^5.0 || ^6.0", "symfony/console": "^5.4 || ^6.0 || ^7.0", "symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0", @@ -2399,16 +2836,16 @@ "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0" }, "require-dev": { - "facile-it/paraunit": "^1.3 || ^2.0", - "infection/infection": "^0.27.11", + "facile-it/paraunit": "^1.3 || ^2.3", + "infection/infection": "^0.29.5", "justinrainbow/json-schema": "^5.2", "keradus/cli-executor": "^2.1", "mikey179/vfsstream": "^1.6.11", "php-coveralls/php-coveralls": "^2.7", "php-cs-fixer/accessible-object": "^1.1", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.4", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.4", - "phpunit/phpunit": "^9.6 || ^10.5.5 || ^11.0.2", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.5", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.5", + "phpunit/phpunit": "^9.6.19 || ^10.5.21 || ^11.2", "symfony/var-dumper": "^5.4 || ^6.0 || ^7.0", "symfony/yaml": "^5.4 || ^6.0 || ^7.0" }, @@ -2423,7 +2860,10 @@ "autoload": { "psr-4": { "PhpCsFixer\\": "src/" - } + }, + "exclude-from-classmap": [ + "src/Fixer/Internal/*" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2448,7 +2888,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.54.0" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.64.0" }, "funding": [ { @@ -2456,137 +2896,550 @@ "type": "github" } ], - "time": "2024-04-17T08:12:13+00:00" + "time": "2024-08-30T23:09:38+00:00" }, { - "name": "phpstan/phpstan", - "version": "1.10.67", + "name": "react/cache", + "version": "v1.2.0", "source": { "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "16ddbe776f10da6a95ebd25de7c1dbed397dc493" + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/16ddbe776f10da6a95ebd25de7c1dbed397dc493", - "reference": "16ddbe776f10da6a95ebd25de7c1dbed397dc493", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", "shasum": "" }, "require": { - "php": "^7.2|^8.0" + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" }, - "conflict": { - "phpstan/phpstan-shim": "*" + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" }, - "bin": [ - "phpstan", - "phpstan.phar" + "type": "library", + "autoload": { + "psr-4": { + "React\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2022-11-30T15:59:55+00:00" + }, + { + "name": "react/child-process", + "version": "v0.6.5", + "source": { + "type": "git", + "url": "https://github.com/reactphp/child-process.git", + "reference": "e71eb1aa55f057c7a4a0d08d06b0b0a484bead43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/e71eb1aa55f057c7a4a0d08d06b0b0a484bead43", + "reference": "e71eb1aa55f057c7a4a0d08d06b0b0a484bead43", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/event-loop": "^1.2", + "react/stream": "^1.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", + "react/socket": "^1.8", + "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" + }, "type": "library", "autoload": { - "files": [ - "bootstrap.php" - ] + "psr-4": { + "React\\ChildProcess\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "PHPStan - PHP Static Analysis Tool", + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven library for executing child processes with ReactPHP.", "keywords": [ - "dev", - "static analysis" + "event-driven", + "process", + "reactphp" ], "support": { - "docs": "https://phpstan.org/user-guide/getting-started", - "forum": "https://github.com/phpstan/phpstan/discussions", - "issues": "https://github.com/phpstan/phpstan/issues", - "security": "https://github.com/phpstan/phpstan/security/policy", - "source": "https://github.com/phpstan/phpstan-src" + "issues": "https://github.com/reactphp/child-process/issues", + "source": "https://github.com/reactphp/child-process/tree/v0.6.5" }, "funding": [ { - "url": "https://github.com/ondrejmirtes", + "url": "https://github.com/WyriHaximus", "type": "github" }, { - "url": "https://github.com/phpstan", + "url": "https://github.com/clue", "type": "github" } ], - "time": "2024-04-16T07:22:02+00:00" + "time": "2022-09-16T13:41:56+00:00" }, { - "name": "rector/rector", - "version": "1.0.4", + "name": "react/dns", + "version": "v1.13.0", "source": { "type": "git", - "url": "https://github.com/rectorphp/rector.git", - "reference": "6e04d0eb087aef707fa0c5686d33d6ff61f4a555" + "url": "https://github.com/reactphp/dns.git", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/6e04d0eb087aef707fa0c5686d33d6ff61f4a555", - "reference": "6e04d0eb087aef707fa0c5686d33d6ff61f4a555", + "url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", "shasum": "" }, "require": { - "php": "^7.2|^8.0", - "phpstan/phpstan": "^1.10.57" + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" }, - "conflict": { - "rector/rector-doctrine": "*", - "rector/rector-downgrade-php": "*", - "rector/rector-phpunit": "*", - "rector/rector-symfony": "*" + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Dns\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.13.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-13T14:18:03+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" }, "suggest": { - "ext-dom": "To manipulate phpunit.xml via the custom-rule command" + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" }, - "bin": [ - "bin/rector" + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-11-13T13:48:05+00:00" + }, + { + "name": "react/promise", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, "type": "library", "autoload": { "files": [ - "bootstrap.php" - ] + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "Instant Upgrade and Automated Refactoring of any PHP code", + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", "keywords": [ - "automation", - "dev", - "migration", - "refactoring" + "promise", + "promises" ], "support": { - "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/1.0.4" + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.2.0" }, "funding": [ { - "url": "https://github.com/tomasvotruba", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-05-24T10:39:05+00:00" + }, + { + "name": "react/socket", + "version": "v1.16.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.16.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-07-26T10:38:09+00:00" + }, + { + "name": "react/stream", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "time": "2024-04-05T09:01:07+00:00" + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" }, { "name": "sebastian/diff", - "version": "6.0.1", + "version": "6.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "ab83243ecc233de5655b76f577711de9f842e712" + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ab83243ecc233de5655b76f577711de9f842e712", - "reference": "ab83243ecc233de5655b76f577711de9f842e712", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", "shasum": "" }, "require": { @@ -2632,7 +3485,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/6.0.1" + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" }, "funding": [ { @@ -2640,20 +3493,20 @@ "type": "github" } ], - "time": "2024-03-02T07:30:33+00:00" + "time": "2024-07-03T04:53:05+00:00" }, { "name": "symfony/console", - "version": "v6.4.6", + "version": "v6.4.12", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "a2708a5da5c87d1d0d52937bdeac625df659e11f" + "reference": "72d080eb9edf80e36c19be61f72c98ed8273b765" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/a2708a5da5c87d1d0d52937bdeac625df659e11f", - "reference": "a2708a5da5c87d1d0d52937bdeac625df659e11f", + "url": "https://api.github.com/repos/symfony/console/zipball/72d080eb9edf80e36c19be61f72c98ed8273b765", + "reference": "72d080eb9edf80e36c19be61f72c98ed8273b765", "shasum": "" }, "require": { @@ -2718,7 +3571,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.6" + "source": "https://github.com/symfony/console/tree/v6.4.12" }, "funding": [ { @@ -2734,20 +3587,20 @@ "type": "tidelift" } ], - "time": "2024-03-29T19:07:53+00:00" + "time": "2024-09-20T08:15:52+00:00" }, { "name": "symfony/options-resolver", - "version": "v7.0.0", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "700ff4096e346f54cb628ea650767c8130f1001f" + "reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/700ff4096e346f54cb628ea650767c8130f1001f", - "reference": "700ff4096e346f54cb628ea650767c8130f1001f", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/47aa818121ed3950acd2b58d1d37d08a94f9bf55", + "reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55", "shasum": "" }, "require": { @@ -2785,7 +3638,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.0.0" + "source": "https://github.com/symfony/options-resolver/tree/v7.1.1" }, "funding": [ { @@ -2801,24 +3654,24 @@ "type": "tidelift" } ], - "time": "2023-08-08T10:20:21+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.29.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f" + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f", - "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" @@ -2863,7 +3716,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" }, "funding": [ { @@ -2879,24 +3732,24 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.29.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "bc45c394692b948b4d383a08d7753968bed9a83d" + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d", - "reference": "bc45c394692b948b4d383a08d7753968bed9a83d", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" @@ -2944,7 +3797,87 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" }, "funding": [ { @@ -2960,24 +3893,24 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-php81", - "version": "v1.29.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "c565ad1e63f30e7477fc40738343c62b40bc672d" + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/c565ad1e63f30e7477fc40738343c62b40bc672d", - "reference": "c565ad1e63f30e7477fc40738343c62b40bc672d", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { @@ -3020,7 +3953,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" }, "funding": [ { @@ -3036,20 +3969,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/process", - "version": "v7.0.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "0e7727191c3b71ebec6d529fa0e50a01ca5679e9" + "reference": "5c03ee6369281177f07f7c68252a280beccba847" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/0e7727191c3b71ebec6d529fa0e50a01ca5679e9", - "reference": "0e7727191c3b71ebec6d529fa0e50a01ca5679e9", + "url": "https://api.github.com/repos/symfony/process/zipball/5c03ee6369281177f07f7c68252a280beccba847", + "reference": "5c03ee6369281177f07f7c68252a280beccba847", "shasum": "" }, "require": { @@ -3081,7 +4014,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.0.4" + "source": "https://github.com/symfony/process/tree/v7.1.5" }, "funding": [ { @@ -3097,20 +4030,20 @@ "type": "tidelift" } ], - "time": "2024-02-22T20:27:20+00:00" + "time": "2024-09-19T21:48:23+00:00" }, { "name": "symfony/stopwatch", - "version": "v7.0.3", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "983900d6fddf2b0cbaacacbbad07610854bd8112" + "reference": "5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/983900d6fddf2b0cbaacacbbad07610854bd8112", - "reference": "983900d6fddf2b0cbaacacbbad07610854bd8112", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d", + "reference": "5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d", "shasum": "" }, "require": { @@ -3143,7 +4076,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v7.0.3" + "source": "https://github.com/symfony/stopwatch/tree/v7.1.1" }, "funding": [ { @@ -3159,20 +4092,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T15:02:46+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/string", - "version": "v7.0.4", + "version": "v7.1.5", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f5832521b998b0bec40bee688ad5de98d4cf111b" + "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f5832521b998b0bec40bee688ad5de98d4cf111b", - "reference": "f5832521b998b0bec40bee688ad5de98d4cf111b", + "url": "https://api.github.com/repos/symfony/string/zipball/d66f9c343fa894ec2037cc928381df90a7ad4306", + "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306", "shasum": "" }, "require": { @@ -3186,6 +4119,7 @@ "symfony/translation-contracts": "<2.5" }, "require-dev": { + "symfony/emoji": "^7.1", "symfony/error-handler": "^6.4|^7.0", "symfony/http-client": "^6.4|^7.0", "symfony/intl": "^6.4|^7.0", @@ -3229,7 +4163,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.0.4" + "source": "https://github.com/symfony/string/tree/v7.1.5" }, "funding": [ { @@ -3245,7 +4179,7 @@ "type": "tidelift" } ], - "time": "2024-02-01T13:17:36+00:00" + "time": "2024-09-20T08:28:38+00:00" } ], "aliases": [], @@ -3254,7 +4188,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^8.2" + "php": "^8.3" }, "platform-dev": [], "plugin-api-version": "2.3.0" diff --git a/lib/php/workflow/src/State/Repository/DoctrineStateRepository.php b/lib/php/workflow/src/State/Repository/DoctrineStateRepository.php index d0892816b..b3ffed877 100644 --- a/lib/php/workflow/src/State/Repository/DoctrineStateRepository.php +++ b/lib/php/workflow/src/State/Repository/DoctrineStateRepository.php @@ -11,7 +11,14 @@ use Doctrine\DBAL\LockMode; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\QueryBuilder; - +use Symfony\Component\EventDispatcher\Attribute\AsEventListener; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; +use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent; + +#[AsEventListener(event: WorkerMessageHandledEvent::class, method: 'clear')] +#[AsEventListener(event: WorkerMessageFailedEvent::class, method: 'clear')] +#[AsEventListener(event: KernelEvents::TERMINATE, method: 'clear')] class DoctrineStateRepository implements LockAwareStateRepositoryInterface { private array $jobs = []; @@ -27,6 +34,11 @@ public function __construct( $this->jobStateEntity = $jobStateEntity ?? JobStateEntity::class; } + public function clear(): void + { + $this->jobs = []; + } + public function getWorkflowState(string $id): WorkflowState { $entity = $this->em->getRepository($this->workflowStateEntity)->find($id); @@ -90,6 +102,8 @@ public function acquireJobLock(string $workflowId, string $jobId): void if ($entity instanceof JobStateEntity) { $this->jobs[$workflowId][$jobId] = $entity; + } else { + unset($this->jobs[$workflowId][$jobId]); } } catch (\Throwable $e) { $this->em->rollback(); @@ -151,6 +165,8 @@ private function fetchJobEntity(string $workflowId, string $jobId): ?JobStateEnt if ($entity) { $this->jobs[$workflowId][$jobId] = $entity; + } else { + unset($this->jobs[$workflowId][$jobId]); } return $entity; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5a0640c57..2f4e24a04 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2731,17 +2731,6 @@ packages: '@types/react': optional: true - '@mui/base@5.0.0-beta.58': - resolution: {integrity: sha512-P0E7ZrxOuyYqBvVv9w8k7wm+Xzx/KRu+BGgFcR2htTsGCpJNQJCSUXNUZ50MUmSU9hzqhwbQWNXhV1MBTl6F7A==} - engines: {node: '>=14.0.0'} - peerDependencies: - '@types/react': ^17.0.0 || ^18.0.0 - react: ^17.0.0 || ^18.0.0 - react-dom: ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - '@mui/core-downloads-tracker@5.16.7': resolution: {integrity: sha512-RtsCt4Geed2/v74sbihWzzRs+HsIQCfclHeORh5Ynu2fS4icIKozcSubwuG7vtzq2uW3fOR1zITSP84TNt2GoQ==} @@ -2848,16 +2837,6 @@ packages: '@types/react': optional: true - '@mui/utils@6.0.0-rc.0': - resolution: {integrity: sha512-tBp0ILEXDL0bbDDT8PnZOjCqSm5Dfk2N0Z45uzRw+wVl6fVvloC9zw8avl+OdX1Bg3ubs/ttKn8nRNv17bpM5A==} - engines: {node: '>=14.0.0'} - peerDependencies: - '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 - react: ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - '@mui/x-tree-view@6.17.0': resolution: {integrity: sha512-09dc2D+Rjg2z8KOaxbUXyPi0aw7fm2jurEtV8Xw48xJ00joLWd5QJm1/v4CarEvaiyhTQzHImNqdgeJW8ZQB6g==} engines: {node: '>=14.0.0'} @@ -14366,20 +14345,6 @@ snapshots: optionalDependencies: '@types/react': 18.3.11 - '@mui/base@5.0.0-beta.58(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@babel/runtime': 7.25.7 - '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mui/types': 7.2.17(@types/react@18.3.11) - '@mui/utils': 6.0.0-rc.0(@types/react@18.3.11)(react@18.3.1) - '@popperjs/core': 2.11.8 - clsx: 2.1.1 - prop-types: 15.8.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - optionalDependencies: - '@types/react': 18.3.11 - '@mui/core-downloads-tracker@5.16.7': {} '@mui/icons-material@5.16.7(@mui/material@5.16.7(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.11)(react@18.3.1)': @@ -14480,24 +14445,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.11 - '@mui/utils@6.0.0-rc.0(@types/react@18.3.11)(react@18.3.1)': - dependencies: - '@babel/runtime': 7.25.7 - '@mui/types': 7.2.17(@types/react@18.3.11) - '@types/prop-types': 15.7.13 - clsx: 2.1.1 - prop-types: 15.8.1 - react: 18.3.1 - react-is: 18.3.1 - optionalDependencies: - '@types/react': 18.3.11 - '@mui/x-tree-view@6.17.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@mui/material@5.16.7(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@5.16.7(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.25.7 '@emotion/react': 11.13.3(@types/react@18.3.11)(react@18.3.1) '@emotion/styled': 11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1) - '@mui/base': 5.0.0-beta.58(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mui/base': 5.0.0-beta.40(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mui/material': 5.16.7(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mui/system': 5.16.7(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1) '@mui/utils': 5.16.6(@types/react@18.3.11)(react@18.3.1)