From e161403d2e6bf7cb9a408af31caaa4ecbd29ffba Mon Sep 17 00:00:00 2001 From: Arthur M <4rthem@users.noreply.github.com> Date: Wed, 31 Jul 2024 19:17:02 +0200 Subject: [PATCH] PS-658 attr batch edit (#445) * refactor attributes * add attribute entity * upgrade keycloak --- bin/setup.sh | 2 +- .../Vendor/Keycloak/KeycloakConfigurator.php | 3 + databox/api/config/packages/fos_elastica.yaml | 62 + databox/api/config/services.yaml | 11 +- databox/api/fixtures/Newspaper.yaml | 904 ++++++++------ .../api/migrations/Version20240702155025.php | 38 + .../api/migrations/Version20240716145148.php | 48 + .../api/migrations/Version20240716150026.php | 32 + .../api/migrations/Version20240716165827.php | 37 + .../api/migrations/Version20240717143101.php | 50 + .../api/migrations/Version20240730141312.php | 32 + .../AbstractExtendedAttributeInput.php | 4 +- .../Input/Attribute/AttributeActionInput.php | 12 + .../Attribute/AttributeBatchUpdateInput.php | 8 +- .../Output/AttributeDefinitionOutput.php | 7 +- .../src/Api/Model/Output/AttributeOutput.php | 16 +- .../AssetOutputTransformer.php | 35 +- .../AttributeDefinitionOutputTransformer.php | 1 + .../AttributeOutputTransformer.php | 14 +- .../AssetAttributeBatchUpdateProcessor.php | 2 +- .../BatchAttributeUpdateProcessor.php | 25 +- .../AttributeEntityCollectionProvider.php | 45 + .../Api/Provider/TagCollectionProvider.php | 45 + .../api/src/Api/Traits/UserLocaleTrait.php | 4 +- databox/api/src/Asset/AssetCopier.php | 4 +- .../Asset/Attribute/AssetTitleResolver.php | 19 +- .../Asset/Attribute/AttributesResolver.php | 135 +- .../Asset/Attribute/DynamicAttributeBag.php | 16 +- .../src/Asset/Attribute/FallbackResolver.php | 20 +- .../Asset/Attribute/Index/AttributeIndex.php | 49 + .../Asset/Attribute/Index/DefinitionIndex.php | 54 + .../InitialAttributeValuesResolver.php | 4 +- .../api/src/Attribute/AttributeAssigner.php | 2 +- .../api/src/Attribute/AttributeInterface.php | 9 + .../api/src/Attribute/AttributeSplitter.php | 3 +- .../src/Attribute/BatchAttributeManager.php | 149 ++- .../Attribute/Type/AbstractAttributeType.php | 5 + .../Attribute/Type/AttributeTypeInterface.php | 5 + .../Attribute/Type/EntityAttributeType.php | 129 ++ .../Handler/Search/AttributeEntityDelete.php | 31 + .../Search/AttributeEntityDeleteHandler.php | 95 ++ .../Handler/Search/AttributeEntityUpdate.php | 25 + .../Search/AttributeEntityUpdateHandler.php | 136 ++ .../Handler/Search/ESPopulateHandler.php | 2 - .../Admin/AttributeCrudController.php | 62 +- .../AttributeDefinitionCrudController.php | 1 + .../Admin/AttributeEntityCrudController.php | 59 + .../Controller/Admin/DashboardController.php | 2 + .../Core/AttributeBatchUpdateAction.php | 38 - .../ExposeIntegrationController.php | 1 - .../Listener/AttributeEntityListener.php | 70 ++ .../Listener/CacheInvalidatorListener.php | 68 - .../MemoryCacheInvalidatorListener.php | 31 - databox/api/src/Elasticsearch/AssetSearch.php | 11 +- .../Elasticsearch/AttributeEntitySearch.php | 75 ++ .../api/src/Elasticsearch/AttributeSearch.php | 21 +- .../src/Elasticsearch/ElasticSearchClient.php | 39 + .../Listener/AssetPostTransformListener.php | 47 +- .../Mapping/FieldNameResolver.php | 3 +- .../Mapping/IndexMappingUpdater.php | 27 +- .../src/Elasticsearch/SuggestionSearch.php | 4 +- databox/api/src/Elasticsearch/TagSearch.php | 70 ++ databox/api/src/Entity/Basket/BasketAsset.php | 12 +- .../src/Entity/Core/AbstractBaseAttribute.php | 15 - .../api/src/Entity/Core/AssetRendition.php | 18 + databox/api/src/Entity/Core/Attribute.php | 83 +- .../src/Entity/Core/AttributeDefinition.php | 26 +- .../api/src/Entity/Core/AttributeEntity.php | 119 ++ databox/api/src/Entity/Core/Tag.php | 5 +- .../Traits/AssetAnnotationsInterface.php | 29 + .../Entity/Traits/AssetAnnotationsTrait.php | 32 + .../Fixture/Faker/AssetAnnotationsFaker.php | 84 ++ .../Core/Watermark/WatermarkAction.php | 7 +- .../Phrasea/Expose/ExposeClient.php | 69 +- .../Phrasea/Expose/ExposeSynchronizer.php | 2 +- ...initionRepositoryMemoryCachedDecorator.php | 86 -- .../Repository/Cache/CacheDecoratorTrait.php | 49 - .../Cache/CacheRepositoryInterface.php | 12 - .../Core/AttributeDefinitionRepository.php | 33 +- ...AttributeDefinitionRepositoryInterface.php | 40 - .../Core/AttributeEntityRepository.php | 28 + .../Repository/Core/AttributeRepository.php | 31 +- .../Core/AttributeRepositoryInterface.php | 3 +- .../api/src/Repository/Core/TagRepository.php | 28 + .../Voter/AttributeDefinitionVoter.php | 5 + .../Security/Voter/AttributeEntityVoter.php | 39 + .../api/src/Security/Voter/AttributeVoter.php | 10 +- databox/api/src/Storage/RenditionManager.php | 2 +- .../Api/AssetAttributeBatchUpdateTest.php | 17 +- .../tests/Api/AttributeBatchUpdateTest.php | 28 +- .../Api/CreateAssetWithAttributeTest.php | 19 +- databox/api/tests/Api/TagTest.php | 18 +- databox/api/translations/messages.en.yaml | 1 + databox/client/package.json | 1 + databox/client/src/api/asset.ts | 45 +- databox/client/src/api/attributeEntity.ts | 48 + databox/client/src/api/attributes.ts | 3 + databox/client/src/components/App.tsx | 57 +- .../components/AssetList/AssetContextMenu.tsx | 120 +- .../src/components/AssetList/AssetList.tsx | 77 +- .../AssetList/Layouts/Grid/AssetItem.tsx | 15 +- .../AssetList/Layouts/Grid/GridLayout.tsx | 2 + .../AssetList/Layouts/Grid/GridPage.tsx | 2 + .../AssetList/Layouts/GroupDivider.tsx | 1 - .../AssetList/Layouts/List/AssetItem.tsx | 5 +- .../components/AssetList/PreviewPopover.tsx | 2 +- .../AssetList/Toolbar/SelectionActions.tsx | 272 ++-- .../src/components/AssetList/actionContext.ts | 15 + .../client/src/components/AssetList/types.ts | 34 +- .../components/AttributeEditor/AssetItem.tsx | 68 + .../AttributeEditor/AssetToggleOverlay.tsx | 47 + .../AttributeEditor/AttributeEditor.tsx | 377 ++++++ .../AttributeEditor/AttributeEditorLoader.tsx | 60 + .../AttributeEditor/AttributeEditorView.tsx | 44 + .../AttributeEditor/AttributeWidget.tsx | 55 + .../components/AttributeEditor/Attributes.tsx | 89 ++ .../AttributeEditor/AttributesToolbar.tsx | 96 ++ .../AttributeEditor/DefinitionsSkeleton.tsx | 11 + .../AttributeEditor/EditorPanel.tsx | 172 +++ .../AttributeEditor/MultiAttributeRow.tsx | 261 ++++ .../AttributeEditor/PartPercentage.tsx | 71 ++ .../components/AttributeEditor/Resizable.tsx | 62 + .../AttributeEditor/SavePreviewDialog.tsx | 97 ++ .../AttributeEditor/Suggestions/Preview.tsx | 130 ++ .../Suggestions/SuggestionPanel.tsx | 46 + .../Suggestions/ValuesSuggestions.tsx | 270 ++++ .../components/AttributeEditor/ValueDiff.tsx | 148 +++ .../AttributeEditor/attributeGroup.ts | 454 +++++++ .../AttributeEditor/batchActions.ts | 306 +++++ .../components/AttributeEditor/shortcuts.ts | 42 + .../AttributeEditor/store/definitionValues.ts | 114 ++ .../AttributeEditor/store/helper.ts | 22 + .../AttributeEditor/store/normalize.ts | 8 + .../AttributeEditor/store/values.ts | 132 ++ .../src/components/AttributeEditor/types.ts | 107 ++ .../CreateAttributeEntityDialog.tsx | 137 ++ .../src/components/Basket/BasketMenuItem.tsx | 2 +- .../components/Basket/BasketViewDialog.tsx | 56 +- .../src/components/Dialog/Asset/EditAsset.tsx | 1 + .../components/Dialog/Tabbed/TabbedDialog.tsx | 104 +- .../src/components/Dialog/Tabbed/tabTypes.ts | 22 + .../Workspace/AttributeClassManager.tsx | 4 +- .../Workspace/AttributeDefinitionManager.tsx | 8 +- .../Workspace/AttributeEntityManager.tsx | 123 ++ .../Dialog/Workspace/DefinitionManager.tsx | 13 +- .../Workspace/RenditionClassManager.tsx | 4 +- .../Workspace/RenditionDefinitionManager.tsx | 8 +- .../Dialog/Workspace/TagManager.tsx | 2 +- .../Dialog/Workspace/WorkspaceDialog.tsx | 13 + .../components/Form/AttributeEntitySelect.tsx | 88 ++ .../client/src/components/Form/TagSelect.tsx | 45 +- .../src/components/Form/WorkspaceForm.tsx | 2 +- .../Expose/CreatePublicationDialog.tsx | 2 +- .../Media/Asset/Actions/CopyAssetsDialog.tsx | 2 +- .../Annotations/AssetAnnotationsOverlay.tsx | 42 + .../Asset/Annotations/CircleAnnotation.tsx | 34 + .../Asset/Annotations/PointAnnotation.tsx | 24 + .../Asset/Annotations/RectAnnotation.tsx | 34 + .../Media/Asset/AssetAttributes.tsx | 51 + .../src/components/Media/Asset/AssetView.tsx | 41 +- .../Asset/Attribute/AttributeHighlights.tsx | 75 ++ .../Media/Asset/Attribute/AttributeRowUI.tsx | 96 +- .../Media/Asset/Attribute/AttributeType.tsx | 7 +- .../Media/Asset/Attribute/AttributeWidget.tsx | 13 + .../Media/Asset/Attribute/Attributes.tsx | 160 +-- .../Asset/Attribute/AttributesEditorForm.tsx | 85 +- .../Media/Asset/Attribute/CopyAttribute.tsx | 5 +- .../Asset/Attribute/MultiAttributeRow.tsx | 4 + .../Attribute/TranslatableAttributeTabs.tsx | 5 + .../Media/Asset/Attribute/attributeIndex.ts | 68 + .../Attribute/types/AttributeEntityType.tsx | 62 + .../Media/Asset/Attribute/types/BaseType.tsx | 6 +- .../Asset/Attribute/types/BooleanType.tsx | 6 +- .../Media/Asset/Attribute/types/CodeType.tsx | 24 +- .../Media/Asset/Attribute/types/ColorType.tsx | 4 +- .../Asset/Attribute/types/GeoPointType.tsx | 5 +- .../Media/Asset/Attribute/types/TagsType.tsx | 56 + .../Media/Asset/Attribute/types/TextType.tsx | 51 +- .../Media/Asset/Attribute/types/index.ts | 6 +- .../Media/Asset/Attribute/types/types.d.ts | 23 +- .../Asset/Attribute/useAttributeEditor.ts | 12 +- .../src/components/Media/Asset/Facets.tsx | 20 +- .../Media/Asset/Players/PDFPlayer.tsx | 4 +- .../Media/Asset/Players/VideoPlayer.tsx | 4 +- .../components/Media/CollectionMenuItem.tsx | 4 +- .../components/Media/Search/AutoComplete.tsx | 2 +- .../Media/Search/SearchAutoComplete.tsx | 160 +++ .../src/components/Media/Search/SearchBar.tsx | 154 +-- .../Media/TagFilterRule/FilterRule.tsx | 2 + databox/client/src/components/Root.tsx | 28 +- databox/client/src/components/Ui/Flag.tsx | 3 + databox/client/src/components/Ui/Tabs.tsx | 75 ++ .../components/Upload/SaveAsTemplateForm.tsx | 2 +- .../src/components/Upload/UploadForm.tsx | 1 + .../Preferences/UserPreferencesProvider.tsx | 4 +- .../src/components/Workflow/WorkflowView.tsx | 6 +- databox/client/src/constants.ts | 1 + .../client/src/context/WorkspaceContext.tsx | 8 + databox/client/src/hooks/useSelectAllKey.ts | 39 + databox/client/src/routes.ts | 7 +- databox/client/src/types.ts | 43 +- databox/client/src/utils/array.ts | 7 + databox/client/src/utils/types.ts | 1 + docker-compose.dev.yml | 11 - docker-compose.yml | 11 +- infra/docker/keycloak/Dockerfile | 9 +- .../themes/phrasea/account/theme.properties | 2 +- lib/js/api/index.ts | 3 +- lib/js/api/src/utils.ts | 18 + lib/js/navigation/index.ts | 3 +- .../navigation/src/Overlay/OverlayOutlet.tsx | 7 +- .../navigation/src/useNavigateToOverlay.tsx | 14 +- .../src/components/Dialog/AppDialog.tsx | 1 + lib/js/react-form/index.ts | 10 +- lib/js/react-form/src/AsyncRSelectWidget.tsx | 3 +- lib/js/react-form/src/RSelectWidget.tsx | 1 + .../Translations/KeyTranslationsWidget.tsx | 80 ++ .../react-form/src/Widget}/CheckboxWidget.tsx | 0 .../react-form/src/Widget}/SwitchWidget.tsx | 0 lib/js/react-hooks/package.json | 9 +- lib/js/react-hooks/src/deep.ts | 27 + lib/js/react-hooks/src/useDebounce.ts | 14 + lib/js/react-hooks/src/useEffectOnce.ts | 12 +- lib/js/react-hooks/src/useElementResize.ts | 37 + lib/js/react-hooks/src/useMountEffect.ts | 12 + lib/js/react-hooks/src/useUpdateEffect.ts | 13 + lib/js/react-hooks/src/utils.ts | 10 + pnpm-lock.yaml | 1107 +++++++++-------- .../Controller/Admin/CommitCrudController.php | 10 +- 229 files changed, 8920 insertions(+), 2610 deletions(-) create mode 100644 databox/api/migrations/Version20240702155025.php create mode 100644 databox/api/migrations/Version20240716145148.php create mode 100644 databox/api/migrations/Version20240716150026.php create mode 100644 databox/api/migrations/Version20240716165827.php create mode 100644 databox/api/migrations/Version20240717143101.php create mode 100644 databox/api/migrations/Version20240730141312.php create mode 100644 databox/api/src/Api/Provider/AttributeEntityCollectionProvider.php create mode 100644 databox/api/src/Api/Provider/TagCollectionProvider.php create mode 100644 databox/api/src/Asset/Attribute/Index/AttributeIndex.php create mode 100644 databox/api/src/Asset/Attribute/Index/DefinitionIndex.php create mode 100644 databox/api/src/Attribute/AttributeInterface.php create mode 100644 databox/api/src/Attribute/Type/EntityAttributeType.php create mode 100644 databox/api/src/Consumer/Handler/Search/AttributeEntityDelete.php create mode 100644 databox/api/src/Consumer/Handler/Search/AttributeEntityDeleteHandler.php create mode 100644 databox/api/src/Consumer/Handler/Search/AttributeEntityUpdate.php create mode 100644 databox/api/src/Consumer/Handler/Search/AttributeEntityUpdateHandler.php create mode 100644 databox/api/src/Controller/Admin/AttributeEntityCrudController.php delete mode 100644 databox/api/src/Controller/Core/AttributeBatchUpdateAction.php create mode 100644 databox/api/src/Doctrine/Listener/AttributeEntityListener.php delete mode 100644 databox/api/src/Doctrine/Listener/CacheInvalidatorListener.php delete mode 100644 databox/api/src/Doctrine/Listener/MemoryCacheInvalidatorListener.php create mode 100644 databox/api/src/Elasticsearch/AttributeEntitySearch.php create mode 100644 databox/api/src/Elasticsearch/ElasticSearchClient.php create mode 100644 databox/api/src/Elasticsearch/TagSearch.php create mode 100644 databox/api/src/Entity/Core/AttributeEntity.php create mode 100644 databox/api/src/Entity/Traits/AssetAnnotationsInterface.php create mode 100644 databox/api/src/Entity/Traits/AssetAnnotationsTrait.php create mode 100644 databox/api/src/Fixture/Faker/AssetAnnotationsFaker.php delete mode 100644 databox/api/src/Repository/Cache/AttributeDefinitionRepositoryMemoryCachedDecorator.php delete mode 100644 databox/api/src/Repository/Cache/CacheDecoratorTrait.php delete mode 100644 databox/api/src/Repository/Cache/CacheRepositoryInterface.php delete mode 100644 databox/api/src/Repository/Core/AttributeDefinitionRepositoryInterface.php create mode 100644 databox/api/src/Repository/Core/AttributeEntityRepository.php create mode 100644 databox/api/src/Repository/Core/TagRepository.php create mode 100644 databox/api/src/Security/Voter/AttributeEntityVoter.php create mode 100644 databox/client/src/api/attributeEntity.ts create mode 100644 databox/client/src/components/AssetList/actionContext.ts create mode 100644 databox/client/src/components/AttributeEditor/AssetItem.tsx create mode 100644 databox/client/src/components/AttributeEditor/AssetToggleOverlay.tsx create mode 100644 databox/client/src/components/AttributeEditor/AttributeEditor.tsx create mode 100644 databox/client/src/components/AttributeEditor/AttributeEditorLoader.tsx create mode 100644 databox/client/src/components/AttributeEditor/AttributeEditorView.tsx create mode 100644 databox/client/src/components/AttributeEditor/AttributeWidget.tsx create mode 100644 databox/client/src/components/AttributeEditor/Attributes.tsx create mode 100644 databox/client/src/components/AttributeEditor/AttributesToolbar.tsx create mode 100644 databox/client/src/components/AttributeEditor/DefinitionsSkeleton.tsx create mode 100644 databox/client/src/components/AttributeEditor/EditorPanel.tsx create mode 100644 databox/client/src/components/AttributeEditor/MultiAttributeRow.tsx create mode 100644 databox/client/src/components/AttributeEditor/PartPercentage.tsx create mode 100644 databox/client/src/components/AttributeEditor/Resizable.tsx create mode 100644 databox/client/src/components/AttributeEditor/SavePreviewDialog.tsx create mode 100644 databox/client/src/components/AttributeEditor/Suggestions/Preview.tsx create mode 100644 databox/client/src/components/AttributeEditor/Suggestions/SuggestionPanel.tsx create mode 100644 databox/client/src/components/AttributeEditor/Suggestions/ValuesSuggestions.tsx create mode 100644 databox/client/src/components/AttributeEditor/ValueDiff.tsx create mode 100644 databox/client/src/components/AttributeEditor/attributeGroup.ts create mode 100644 databox/client/src/components/AttributeEditor/batchActions.ts create mode 100644 databox/client/src/components/AttributeEditor/shortcuts.ts create mode 100644 databox/client/src/components/AttributeEditor/store/definitionValues.ts create mode 100644 databox/client/src/components/AttributeEditor/store/helper.ts create mode 100644 databox/client/src/components/AttributeEditor/store/normalize.ts create mode 100644 databox/client/src/components/AttributeEditor/store/values.ts create mode 100644 databox/client/src/components/AttributeEditor/types.ts create mode 100644 databox/client/src/components/AttributeEntity/CreateAttributeEntityDialog.tsx create mode 100644 databox/client/src/components/Dialog/Tabbed/tabTypes.ts create mode 100644 databox/client/src/components/Dialog/Workspace/AttributeEntityManager.tsx create mode 100644 databox/client/src/components/Form/AttributeEntitySelect.tsx create mode 100644 databox/client/src/components/Media/Asset/Annotations/AssetAnnotationsOverlay.tsx create mode 100644 databox/client/src/components/Media/Asset/Annotations/CircleAnnotation.tsx create mode 100644 databox/client/src/components/Media/Asset/Annotations/PointAnnotation.tsx create mode 100644 databox/client/src/components/Media/Asset/Annotations/RectAnnotation.tsx create mode 100644 databox/client/src/components/Media/Asset/AssetAttributes.tsx create mode 100644 databox/client/src/components/Media/Asset/Attribute/AttributeHighlights.tsx create mode 100644 databox/client/src/components/Media/Asset/Attribute/attributeIndex.ts create mode 100644 databox/client/src/components/Media/Asset/Attribute/types/AttributeEntityType.tsx create mode 100644 databox/client/src/components/Media/Asset/Attribute/types/TagsType.tsx create mode 100644 databox/client/src/components/Media/Search/SearchAutoComplete.tsx create mode 100644 databox/client/src/components/Ui/Tabs.tsx create mode 100644 databox/client/src/constants.ts create mode 100644 databox/client/src/context/WorkspaceContext.tsx create mode 100644 databox/client/src/hooks/useSelectAllKey.ts create mode 100644 databox/client/src/utils/array.ts create mode 100644 databox/client/src/utils/types.ts create mode 100644 lib/js/react-form/src/Translations/KeyTranslationsWidget.tsx rename {databox/client/src/components/Form => lib/js/react-form/src/Widget}/CheckboxWidget.tsx (100%) rename {databox/client/src/components/Form => lib/js/react-form/src/Widget}/SwitchWidget.tsx (100%) create mode 100644 lib/js/react-hooks/src/deep.ts create mode 100644 lib/js/react-hooks/src/useDebounce.ts create mode 100644 lib/js/react-hooks/src/useElementResize.ts create mode 100644 lib/js/react-hooks/src/useMountEffect.ts create mode 100644 lib/js/react-hooks/src/useUpdateEffect.ts create mode 100644 lib/js/react-hooks/src/utils.ts diff --git a/bin/setup.sh b/bin/setup.sh index 79d5eca99..6f05f526b 100755 --- a/bin/setup.sh +++ b/bin/setup.sh @@ -107,7 +107,7 @@ COMPOSE_PROFILES="${COMPOSE_PROFILES},setup" docker compose run --rm -T --entryp " docker compose restart keycloak -docker compose run --rm dockerize -wait http://keycloak:8080 -timeout 200s +docker compose run --rm dockerize -wait http://keycloak:9000/health/ready -timeout 200s PRESETS="" for p in $@; do diff --git a/configurator/src/Configurator/Vendor/Keycloak/KeycloakConfigurator.php b/configurator/src/Configurator/Vendor/Keycloak/KeycloakConfigurator.php index 7c2b6a049..1733af657 100644 --- a/configurator/src/Configurator/Vendor/Keycloak/KeycloakConfigurator.php +++ b/configurator/src/Configurator/Vendor/Keycloak/KeycloakConfigurator.php @@ -123,7 +123,10 @@ public function configure(OutputInterface $output, array $presets): void $defaultAdmin = $this->keycloakManager->createUser([ 'username' => getenv('DEFAULT_ADMIN_USERNAME'), + 'email' => getenv('DEFAULT_ADMIN_USERNAME').'@'.getenv('PHRASEA_DOMAIN'), 'enabled' => true, + 'firstName' => 'Admin', + 'lastName' => 'Admin', 'credentials' => [[ 'type' => 'password', 'value' => getenv('DEFAULT_ADMIN_PASSWORD'), diff --git a/databox/api/config/packages/fos_elastica.yaml b/databox/api/config/packages/fos_elastica.yaml index a1a380f9a..6c4ed1c09 100644 --- a/databox/api/config/packages/fos_elastica.yaml +++ b/databox/api/config/packages/fos_elastica.yaml @@ -310,6 +310,68 @@ fos_elastica: provider: query_builder_method: getESQueryBuilder + attribute_entity: + settings: + index: + analysis: + analyzer: + text: *text_analyzer + filter: + worddelimiter: *worddelimiter_filter + use_alias: '%elastica.use_alias%' + index_name: "%es_index_prefix%attribute_entity_%kernel.environment%" + properties: + type: + type: keyword + value: + type: text + analyzer: text + fields: + suggest: + type: search_as_you_type + doc_values: false + max_shingle_size: 3 + raw: + type: keyword + workspaceId: + type: keyword + persistence: + driver: orm + model: App\Entity\Core\AttributeEntity + listener: { enabled: false } + provider: + query_builder_method: getESQueryBuilder + + tag: + settings: + index: + analysis: + analyzer: + text: *text_analyzer + filter: + worddelimiter: *worddelimiter_filter + use_alias: '%elastica.use_alias%' + index_name: "%es_index_prefix%tag_%kernel.environment%" + properties: + name: + type: text + analyzer: text + fields: + suggest: + type: search_as_you_type + doc_values: false + max_shingle_size: 3 + raw: + type: keyword + workspaceId: + type: keyword + persistence: + driver: orm + model: App\Entity\Core\Tag + listener: { enabled: false } + provider: + query_builder_method: getESQueryBuilder + when@dev: parameters: elastica.use_alias: false diff --git a/databox/api/config/services.yaml b/databox/api/config/services.yaml index 90642e862..6e28f85a4 100644 --- a/databox/api/config/services.yaml +++ b/databox/api/config/services.yaml @@ -22,6 +22,8 @@ services: ApiPlatform\State\ProviderInterface $collectionProvider: '@api_platform.doctrine.orm.state.collection_provider' bool $useAlias: '%elastica.use_alias%' string $kernelEnv: '%kernel.environment%' + Elastica\Index $assetIndex: '@fos_elastica.index.asset' + Elastica\Index $collectionIndex: '@fos_elastica.index.collection' _instanceof: Alchemy\Workflow\Executor\Action\ActionInterface: @@ -78,12 +80,6 @@ services: $configManager: '@fos_elastica.config_manager' $mappingBuilder: '@fos_elastica.mapping_builder' - App\Elasticsearch\IndexCleaner: - arguments: - $client: '@fos_elastica.client' - $collectionIndex: '@fos_elastica.index.collection' - $assetIndex: '@fos_elastica.index.asset' - fos_elastica.elastica_to_model_transformer.prototype.orm: class: App\Elasticsearch\Transformer\AppElasticaToModelTransformer abstract: true @@ -98,9 +94,6 @@ services: class: App\Serializer\HydraMetaNormalizer public: true - App\Repository\Cache\AttributeDefinitionRepositoryMemoryCachedDecorator: - decorates: 'App\Repository\Core\AttributeDefinitionRepository' - App\Api\Serializer\GroupNormalizerContextBuilder: decorates: 'alchemy_webhook.normalizer.context_builder' arguments: [ '@App\Api\Serializer\GroupNormalizerContextBuilder.inner' ] diff --git a/databox/api/fixtures/Newspaper.yaml b/databox/api/fixtures/Newspaper.yaml index 30b70c210..4a9b07d5c 100644 --- a/databox/api/fixtures/Newspaper.yaml +++ b/databox/api/fixtures/Newspaper.yaml @@ -1,459 +1,545 @@ App\Entity\Core\Workspace: - w_newspaper: - name: Newspaper - slug: newspaper - ownerId: + w_newspaper: + name: Newspaper + slug: newspaper + ownerId: + enabledLocales: + - en + - fr App\Entity\Core\AttributeClass: - attribute_class_n (template): - workspace: '@w_newspaper' - - n_ac_public (extends attribute_class_n): - name: Public - public: true - editable: true - n_ac_business (extends attribute_class_n): - name: Business field - public: false - editable: false + attribute_class_n (template): + workspace: '@w_newspaper' + + n_ac_public (extends attribute_class_n): + name: Public + public: true + editable: true + n_ac_business (extends attribute_class_n): + name: Business field + public: false + editable: false App\Entity\Core\AttributeDefinition: - attribute_definition_n (template): - workspace: '@w_newspaper' - - ad_desc (extends attribute_definition_n): - name: Description - class: '@n_ac_public' - initialValuesAll: "{\"type\": \"metadata\", \"value\": \"XMP-dc:Description\"}" - position: 0 - ad_keywords (extends attribute_definition_n): - name: Keywords - multiple: true - facetEnabled: true - suggest: true - class: '@n_ac_public' - initialValuesAll: "{\"type\": \"metadata\", \"value\": \"IPTC:Keywords\"}" - position: 1 - ad_author (extends attribute_definition_n): - name: Author - multiple: false - facetEnabled: true - class: '@n_ac_public' - initialValuesAll: "{\"type\": \"template\", \"value\": \"{{ file.metadata('IFD0:Artist').value ?? file.metadata('XMP-dc:Creator').value ?? file.metadata('IFD0:Copyright').value ?? file.metadata('IPTC:CopyrightNotice').value }}\"}" - position: 2 - ad_image_size (extends attribute_definition_n): - name: Image Size - multiple: false - class: '@n_ac_public' - initialValuesAll: "{\"type\": \"template\", \"value\": \"{{ file.metadata('Composite:ImageSize').value }}\"}" - position: 2 - ad_country (extends attribute_definition_n): - name: Country - multiple: false - facetEnabled: true - class: '@n_ac_public' - initialValuesAll: "{\"type\": \"template\", \"value\": \"{{ file.metadata('IPTC:Country-PrimaryLocationName').value ?? file.metadata('XMP-photoshop:Country').value }}\"}" - position: 2 - ad_city (extends attribute_definition_n): - name: City - multiple: false - facetEnabled: true - class: '@n_ac_public' - initialValuesAll: "{\"type\": \"template\", \"value\": \"{{ file.metadata('IPTC:City').value ?? file.metadata('XMP-photoshop:City').value }}\"}" - position: 3 - ad_creation_date (extends attribute_definition_n): - name: CreationDate - multiple: false - facetEnabled: false - class: '@n_ac_public' - initialValuesAll: "{\"type\": \"template\", \"value\": \"{{ file.metadata('ExifIFD:CreateDate').value ?? file.metadata('IPTC:DateCreated').value ?? file.metadata('IPTC:DigitalCreationDate').value }}\"}" - position: 4 - ad_admin_note (extends attribute_definition_n): - name: Admin notes - class: '@n_ac_business' - position: 5 - ad_vtt (extends attribute_definition_n): - name: WebVTT - fieldType: code - class: '@n_ac_business' - translatable: true - position: 6 - ad_date (extends attribute_definition_n): - name: Date - fieldType: date - facetEnabled: true - sortable: true - class: '@n_ac_public' - position: 7 - ad_datetime (extends attribute_definition_n): - name: 'Date & Time' - fieldType: date_time - facetEnabled: true - sortable: true - class: '@n_ac_public' - position: 8 - ad_languages (extends attribute_definition_n): - name: Languages - facetEnabled: true - multiple: true - class: '@n_ac_public' - position: 9 - ad_watermark (extends attribute_definition_n): - name: Watermark - class: '@n_ac_public' - position: 10 - ad_blurhash (extends attribute_definition_n): - name: Blurhash - class: '@n_ac_public' - position: 10 - ad_ean (extends attribute_definition_n): - name: EAN - facetEnabled: true - suggest: true - class: '@n_ac_public' - position: 11 - ad_number (extends attribute_definition_n): - name: 'Number' - fieldType: number - facetEnabled: true - sortable: true - suggest: true - class: '@n_ac_public' - position: 12 + attribute_definition_n (template): + workspace: '@w_newspaper' + + ad_desc (extends attribute_definition_n): + name: Description + class: '@n_ac_public' + initialValuesAll: "{\"type\": \"metadata\", \"value\": \"XMP-dc:Description\"}" + position: 0 + ad_keywords (extends attribute_definition_n): + name: Keywords + multiple: true + facetEnabled: true + suggest: true + class: '@n_ac_public' + initialValuesAll: "{\"type\": \"metadata\", \"value\": \"IPTC:Keywords\"}" + position: 1 + ad_author (extends attribute_definition_n): + name: Author + multiple: false + facetEnabled: true + class: '@n_ac_public' + initialValuesAll: "{\"type\": \"template\", \"value\": \"{{ file.metadata('IFD0:Artist').value ?? file.metadata('XMP-dc:Creator').value ?? file.metadata('IFD0:Copyright').value ?? file.metadata('IPTC:CopyrightNotice').value }}\"}" + position: 2 + ad_image_size (extends attribute_definition_n): + name: Image Size + multiple: false + class: '@n_ac_public' + initialValuesAll: "{\"type\": \"template\", \"value\": \"{{ file.metadata('Composite:ImageSize').value }}\"}" + position: 2 + ad_country (extends attribute_definition_n): + name: Country + multiple: false + facetEnabled: true + class: '@n_ac_public' + initialValuesAll: "{\"type\": \"template\", \"value\": \"{{ file.metadata('IPTC:Country-PrimaryLocationName').value ?? file.metadata('XMP-photoshop:Country').value }}\"}" + position: 2 + ad_city (extends attribute_definition_n): + name: City + multiple: false + facetEnabled: true + class: '@n_ac_public' + initialValuesAll: "{\"type\": \"template\", \"value\": \"{{ file.metadata('IPTC:City').value ?? file.metadata('XMP-photoshop:City').value }}\"}" + position: 3 + ad_creation_date (extends attribute_definition_n): + name: CreationDate + multiple: false + facetEnabled: false + class: '@n_ac_public' + initialValuesAll: "{\"type\": \"template\", \"value\": \"{{ file.metadata('ExifIFD:CreateDate').value ?? file.metadata('IPTC:DateCreated').value ?? file.metadata('IPTC:DigitalCreationDate').value }}\"}" + position: 4 + ad_admin_note (extends attribute_definition_n): + name: Admin notes + class: '@n_ac_business' + position: 5 + ad_vtt (extends attribute_definition_n): + name: WebVTT + fieldType: code + class: '@n_ac_business' + translatable: true + position: 6 + ad_date (extends attribute_definition_n): + name: Date + fieldType: date + facetEnabled: true + sortable: true + class: '@n_ac_public' + position: 7 + ad_datetime (extends attribute_definition_n): + name: 'Date & Time' + fieldType: date_time + facetEnabled: true + sortable: true + class: '@n_ac_public' + position: 8 + ad_languages (extends attribute_definition_n): + name: Languages + facetEnabled: true + multiple: true + class: '@n_ac_public' + position: 9 + ad_watermark (extends attribute_definition_n): + name: Watermark + class: '@n_ac_public' + position: 10 + ad_blurhash (extends attribute_definition_n): + name: Blurhash + class: '@n_ac_public' + position: 10 + ad_ean (extends attribute_definition_n): + name: EAN + facetEnabled: true + suggest: true + class: '@n_ac_public' + position: 11 + ad_number (extends attribute_definition_n): + name: 'Number' + fieldType: number + facetEnabled: true + sortable: true + suggest: true + class: '@n_ac_public' + position: 12 + ad_transport_type (extends attribute_definition_n): + name: 'Transport Type' + fieldType: entity + entityType: transport_type + facetEnabled: true + sortable: true + suggest: true + class: '@n_ac_public' + position: 13 + ad_seasons (extends attribute_definition_n): + name: 'Seasons' + fieldType: entity + entityType: season + facetEnabled: true + multiple: true + sortable: true + suggest: true + class: '@n_ac_public' + position: 14 App\Entity\Core\RenditionClass: - rendition_class_n (template): - workspace: '@w_newspaper' + rendition_class_n (template): + workspace: '@w_newspaper' - n_rc_public (extends rendition_class_n): - name: Public - public: true - n_rc_restricted (extends rendition_class_n): - name: Restricted - public: false + n_rc_public (extends rendition_class_n): + name: Public + public: true + n_rc_restricted (extends rendition_class_n): + name: Restricted + public: false App\Entity\Core\RenditionDefinition: - rendition_definition_n (template): - workspace: '@w_newspaper' - - n_rd_original (extends rendition_definition_n): - name: original - class: '@n_rc_public' - pickSourceFile: true - useAsOriginal: true - n_rd_preview (extends rendition_definition_n): - name: preview - class: '@n_rc_public' - useAsPreview: true - n_rd_thumbnail (extends rendition_definition_n): - name: thumbnail - class: '@n_rc_public' - useAsThumbnail: true - n_rd_preview_mobile (extends rendition_definition_n): - name: preview_mobile - class: '@n_rc_public' - useAsPreview: true - priority: 1 - n_rd_thumbnail_mobile (extends rendition_definition_n): - name: thumbnail_mobile - class: '@n_rc_public' - useAsThumbnail: true - priority: 1 - n_rd_thumbnailActive (extends rendition_definition_n): - name: thumbnailActive - class: '@n_rc_public' - useAsThumbnailActive: true + rendition_definition_n (template): + workspace: '@w_newspaper' + + n_rd_original (extends rendition_definition_n): + name: original + class: '@n_rc_public' + pickSourceFile: true + useAsOriginal: true + n_rd_preview (extends rendition_definition_n): + name: preview + class: '@n_rc_public' + useAsPreview: true + n_rd_thumbnail (extends rendition_definition_n): + name: thumbnail + class: '@n_rc_public' + useAsThumbnail: true + n_rd_preview_mobile (extends rendition_definition_n): + name: preview_mobile + class: '@n_rc_public' + useAsPreview: true + priority: 1 + n_rd_thumbnail_mobile (extends rendition_definition_n): + name: thumbnail_mobile + class: '@n_rc_public' + useAsThumbnail: true + priority: 1 + n_rd_thumbnailActive (extends rendition_definition_n): + name: thumbnailActive + class: '@n_rc_public' + useAsThumbnailActive: true # Groups: # reporter_sport_football_league -# - alice -# - harry +# - alice +# - harry # reporter_sport_football_worldcup -# - bob +# - bob # reporter_sport_football -# - jack +# - jack # reporter_sport_tennis -# - amelia -# - harry -# - oliver +# - amelia +# - harry +# - oliver # reporter_entertainment -# - ava +# - ava # admin_newspaper -# - oliver +# - oliver # No group: -# - john_doe -# - super_user +# - john_doe +# - super_user # Collections hierarchy: # sport -# football -# league -# worldcup -# rugby -# tennis +# football +# league +# worldcup +# rugby +# tennis # entertainment -# public_videos -# movies -# series -# tv_shows -# archives -# movies -# series -# tv_shows +# public_videos +# movies +# series +# tv_shows +# archives +# movies +# series +# tv_shows # others App\Entity\Core\Collection: - collection_n (template): - workspace: '@w_newspaper' - ownerId: + collection_n (template): + workspace: '@w_newspaper' + ownerId: - c_n_special_chars (extends collection_n): - title: 'D&D \@][ \++"=' + c_n_special_chars (extends collection_n): + title: 'D&D \@][ \++"=' - c_n_{sport, entertainment} (extends collection_n): - title: '' + c_n_{sport, entertainment} (extends collection_n): + title: '' - c_n_sport_{football, rugby, tennis} (extends collection_n): - parent: '@c_n_sport' - title: '' + c_n_sport_{football, rugby, tennis} (extends collection_n): + parent: '@c_n_sport' + title: '' - c_n_sport_football_{league, worldcup} (extends collection_n): - parent: '@c_n_sport_football' - title: '' + c_n_sport_football_{league, worldcup} (extends collection_n): + parent: '@c_n_sport_football' + title: '' - c_n_entertainment_{archives} (extends collection_n): - parent: '@c_n_entertainment' - title: '' + c_n_entertainment_{archives} (extends collection_n): + parent: '@c_n_entertainment' + title: '' - c_n_entertainment_{public_videos} (extends collection_n): - ownerId: - privacy: 3 - parent: '@c_n_entertainment' - title: '' + c_n_entertainment_{public_videos} (extends collection_n): + ownerId: + privacy: 3 + parent: '@c_n_entertainment' + title: '' - c_n_entertainment_public_videos_{movies, series, shows} (extends collection_n): - ownerId: - parent: '@c_n_entertainment_public_videos' - title: '' + c_n_entertainment_public_videos_{movies, series, shows} (extends collection_n): + ownerId: + parent: '@c_n_entertainment_public_videos' + title: '' - c_n_entertainment_archives_{movies, series, shows} (extends collection_n): - ownerId: - parent: '@c_n_entertainment_archives' - title: '' + c_n_entertainment_archives_{movies, series, shows} (extends collection_n): + ownerId: + parent: '@c_n_entertainment_archives' + title: '' App\Entity\Core\Asset: - asset_n (template): - workspace: '@w_newspaper' - privacy: '' - __calls: - - addToCollection: ['@c_n_*'] - - addTag (80%?): ['@tag_*'] - - addTag (50%?): ['@tag_*'] - - addToCollection (80%?): ['@c_n_sport', true] - - addToCollection (80%?): ['@c_n_entertainment_archives_movies', true] - - addToCollection (80%?): ['@c_n_entertainment_archives_series', true] - - a_img{1..50} (extends asset_n): - title: 'Image #' - ownerId: - source: '@f_img_o' - a_audio{1..10} (extends asset_n): - title: 'Audio #' - ownerId: - source: '@f_audio_o' - a_video{1..10} (extends asset_n): - title: 'Video #' - ownerId: - source: '@f_video_o' - a_pdf{1..10} (extends asset_n): - title: 'PDF #' - ownerId: - source: '@f_pdf_o' + asset_n (template): + workspace: '@w_newspaper' + privacy: '' + __calls: + - addToCollection: ['@c_n_*'] + - addTag (80%?): ['@tag_*'] + - addTag (50%?): ['@tag_*'] + - addToCollection (80%?): ['@c_n_sport', true] + - addToCollection (80%?): ['@c_n_entertainment_archives_movies', true] + - addToCollection (80%?): ['@c_n_entertainment_archives_series', true] + + a_img{1..50} (extends asset_n): + title: 'Image #' + ownerId: + source: '@f_img_o' + a_audio{1..10} (extends asset_n): + title: 'Audio #' + ownerId: + source: '@f_audio_o' + a_video{1..10} (extends asset_n): + title: 'Video #' + ownerId: + source: '@f_video_o' + a_pdf{1..10} (extends asset_n): + title: 'PDF #' + ownerId: + source: '@f_pdf_o' App\Entity\Core\File: - file (template): - workspace: '@w_newspaper' - storage: s3_main - pathPublic: '95%? true : false' - - f_img_o{1..50} (extends file): - type: image/jpeg - path: 'id, , 1000)>' - f_img_p{1..50} (extends file): - type: image/jpeg - path: 'id, , 500)>' - f_img_t{1..50} (extends file): - type: image/jpeg - path: 'id, , 200)>' - f_img_ta{1..10} (extends file): - type: image/jpeg - path: 'id, + 1, 200)>' - f_audio_o{1..10} (extends file): - type: audio/mp3 - path: 'id, "mp3", )>' - f_audio_t{1..10} (extends file): - type: audio/mp3 - path: 'id, "mp3", )>' - f_video_o{1..10} (extends file): - type: video/mp4 - path: 'id, "mp4", )>' - f_video_p{1..10} (extends file): - type: video/mp4 - path: 'id, "mp4", )>' - f_video_t{1..10} (extends file): - type: video/mp4 - path: 'id, "mp4", )>' - f_pdf_o{1..10} (extends file): - type: application/pdf - path: 'id, "pdf", )>' - f_pdf_p{1..10} (extends file): - type: application/pdf - path: 'id, "pdf", )>' + file (template): + workspace: '@w_newspaper' + storage: s3_main + pathPublic: '95%? true : false' + + f_img_o{1..50} (extends file): + type: image/jpeg + path: 'id, , 1000)>' + f_img_p{1..50} (extends file): + type: image/jpeg + path: 'id, , 500)>' + f_img_t{1..50} (extends file): + type: image/jpeg + path: 'id, , 200)>' + f_img_ta{1..10} (extends file): + type: image/jpeg + path: 'id, + 1, 200)>' + f_audio_o{1..10} (extends file): + type: audio/mp3 + path: 'id, "mp3", )>' + f_audio_t{1..10} (extends file): + type: audio/mp3 + path: 'id, "mp3", )>' + f_video_o{1..10} (extends file): + type: video/mp4 + path: 'id, "mp4", )>' + f_video_p{1..10} (extends file): + type: video/mp4 + path: 'id, "mp4", )>' + f_video_t{1..10} (extends file): + type: video/mp4 + path: 'id, "mp4", )>' + f_pdf_o{1..10} (extends file): + type: application/pdf + path: 'id, "pdf", )>' + f_pdf_p{1..10} (extends file): + type: application/pdf + path: 'id, "pdf", )>' App\Entity\Core\AssetRendition: - asset_rendition_n (template): - - ar_img_o{1..50} (extends asset_rendition_n): - definition: '@n_rd_original' - file: '@f_img_o' - asset: '@a_img' - ar_img_p{1..50} (extends asset_rendition_n): - definition: '@n_rd_preview' - file: '@f_img_p' - asset: '@a_img' - ar_img_t{1..50} (extends asset_rendition_n): - definition: '@n_rd_thumbnail' - file: '@f_img_t' - asset: '@a_img' - ar_img_ta{1..10} (extends asset_rendition_n): - definition: '@n_rd_thumbnailActive' - file: '@f_img_ta' - asset: '@a_img' - ar_audio_o{1..10} (extends asset_rendition_n): - definition: '@n_rd_original' - file: '@f_audio_o' - asset: '@a_audio' - ar_audio_t{1..10} (extends asset_rendition_n): - definition: '@n_rd_thumbnail' - file: '@f_audio_t' - asset: '@a_audio' - ar_video_o{1..10} (extends asset_rendition_n): - definition: '@n_rd_original' - file: '@f_video_o' - asset: '@a_video' - ar_video_p{1..10} (extends asset_rendition_n): - definition: '@n_rd_preview' - file: '@f_video_p' - asset: '@a_video' - ar_video_t{1..10} (extends asset_rendition_n): - definition: '@n_rd_thumbnail' - file: '@f_video_t' - asset: '@a_video' - ar_pdf_o{1..10} (extends asset_rendition_n): - definition: '@n_rd_original' - file: '@f_pdf_o' - asset: '@a_pdf' - ar_pdf_p{1..10} (extends asset_rendition_n): - definition: '@n_rd_preview' - file: '@f_pdf_p' - asset: '@a_pdf' + asset_rendition_n (template): + + ar_img_o{1..50} (extends asset_rendition_n): + definition: '@n_rd_original' + file: '@f_img_o' + asset: '@a_img' + ar_img_p{1..50} (extends asset_rendition_n): + definition: '@n_rd_preview' + file: '@f_img_p' + asset: '@a_img' + ar_img_t{1..50} (extends asset_rendition_n): + definition: '@n_rd_thumbnail' + file: '@f_img_t' + asset: '@a_img' + ar_img_ta{1..10} (extends asset_rendition_n): + definition: '@n_rd_thumbnailActive' + file: '@f_img_ta' + asset: '@a_img' + ar_audio_o{1..10} (extends asset_rendition_n): + definition: '@n_rd_original' + file: '@f_audio_o' + asset: '@a_audio' + ar_audio_t{1..10} (extends asset_rendition_n): + definition: '@n_rd_thumbnail' + file: '@f_audio_t' + asset: '@a_audio' + ar_video_o{1..10} (extends asset_rendition_n): + definition: '@n_rd_original' + file: '@f_video_o' + asset: '@a_video' + ar_video_p{1..10} (extends asset_rendition_n): + definition: '@n_rd_preview' + file: '@f_video_p' + asset: '@a_video' + ar_video_t{1..10} (extends asset_rendition_n): + definition: '@n_rd_thumbnail' + file: '@f_video_t' + asset: '@a_video' + ar_pdf_o{1..10} (extends asset_rendition_n): + definition: '@n_rd_original' + file: '@f_pdf_o' + asset: '@a_pdf' + ar_pdf_p{1..10} (extends asset_rendition_n): + definition: '@n_rd_preview' + file: '@f_pdf_p' + asset: '@a_pdf' App\Entity\Core\Attribute: - attribute_n (template): - origin: 0 - - at_desc_{@a_*} (extends attribute_n): - definition: '@ad_desc' - asset: '@a_*' - value: '' - - at_kw_{@a_*} (extends attribute_n): - definition: '@ad_keywords' - asset: '@a_*' - value: '50%? PDF : ' - - at_ean_{@a_*} (extends attribute_n): - definition: '@ad_ean' - asset: '@a_*' - value: '' - - at_city_{@a_*} (extends attribute_n): - definition: '@ad_city' - asset: '@a_*' - value: '' - - at_country_{@a_*} (extends attribute_n): - definition: '@ad_country' - asset: '@a_*' - value: '' - - at_date_{@a_*} (extends attribute_n): - __factory: - 'App\Fixture\DateFixtureFactory::createDateAttribute': - - '' - definition: '@ad_date' - asset: '@a_*' - - at_datetime_{@a_*} (extends attribute_n): - __factory: - 'App\Fixture\DateFixtureFactory::createDateTimeAttribute': - - '' - definition: '@ad_datetime' - asset: '@a_*' + attribute_n (template): + origin: 0 + + at_desc_{@a_*} (extends attribute_n): + definition: '@ad_desc' + asset: '@a_*' + value: '' + + at_kw1_{@a_*} (extends attribute_n): + definition: '@ad_keywords' + asset: '@a_*' + value: 'Tag Rectangle' + assetAnnotations: '' + + at_kw2_{@a_*} (extends attribute_n): + definition: '@ad_keywords' + asset: '@a_*' + value: 'Tag Circle' + assetAnnotations: '' + + at_kw3_{@a_*} (extends attribute_n): + definition: '@ad_keywords' + asset: '@a_*' + value: '' + assetAnnotations: '' + + at_kw4_{@a_*} (extends attribute_n): + definition: '@ad_keywords' + asset: '@a_*' + value: '' + assetAnnotations: '' + + at_ean_{@a_*} (extends attribute_n): + definition: '@ad_ean' + asset: '@a_*' + value: '' + + at_city_{@a_*} (extends attribute_n): + definition: '@ad_city' + asset: '@a_*' + value: '' + + at_country_{@a_*} (extends attribute_n): + definition: '@ad_country' + asset: '@a_*' + value: '' + + at_date_{@a_*} (extends attribute_n): + __factory: + 'App\Fixture\DateFixtureFactory::createDateAttribute': + - '' + definition: '@ad_date' + asset: '@a_*' + + at_datetime_{@a_*} (extends attribute_n): + __factory: + 'App\Fixture\DateFixtureFactory::createDateTimeAttribute': + - '' + definition: '@ad_datetime' + asset: '@a_*' App\Entity\Core\Tag: - tag_offline: - name: offline - color: '#FF0000' - workspace: '@w_newspaper' + tag_offline: + name: offline + color: '#FF0000' + workspace: '@w_newspaper' - tag_online: - name: online - color: '#7EF284' - workspace: '@w_newspaper' + tag_online: + name: online + color: '#7EF284' + workspace: '@w_newspaper' - tag_{embargo_it, embargo_fr}: - name: '' - workspace: '@w_newspaper' + tag_{embargo_it, embargo_fr}: + name: '' + workspace: '@w_newspaper' App\Entity\Integration\WorkspaceIntegration: - wi_toast_ui: - title: ToastUI - integration: tui.photo-editor - workspace: '@w_newspaper' - wi_renditions: - integration: phraseanet.renditions - workspace: '@w_newspaper' - config: - databoxId: 1 - method: api - renditions: - - thumbnail - - preview - wi_watermark: - integration: core.watermark - workspace: '@w_newspaper' - config: - attributeName: watermark - applyToRenditions: - - thumbnail - needs: - - '@wi_renditions' - wi_blurhash: - integration: blurhash - workspace: '@w_newspaper' - config: - rendition: thumbnail - needs: - - '@wi_renditions' + wi_toast_ui: + title: ToastUI + integration: tui.photo-editor + workspace: '@w_newspaper' + wi_renditions: + integration: phraseanet.renditions + workspace: '@w_newspaper' + config: + databoxId: 1 + method: api + renditions: + - thumbnail + - preview + wi_watermark: + integration: core.watermark + workspace: '@w_newspaper' + config: + attributeName: watermark + applyToRenditions: + - thumbnail + needs: + - '@wi_renditions' + wi_blurhash: + integration: blurhash + workspace: '@w_newspaper' + config: + rendition: thumbnail + needs: + - '@wi_renditions' + wi_expose: + title: Expose Basket + integration: phrasea.expose + config: + clientId: expose-integration App\Entity\Integration\WorkspaceSecret: - aws_access_key_id: - workspace: '@w_newspaper' - name: AWS_ACCESS_KEY_ID - plainValue: xxx - - aws_access_key_secret: - workspace: '@w_newspaper' - name: AWS_ACCESS_KEY_SECRET - plainValue: xxx + aws_access_key_id: + workspace: '@w_newspaper' + name: AWS_ACCESS_KEY_ID + plainValue: xxx + + aws_access_key_secret: + workspace: '@w_newspaper' + name: AWS_ACCESS_KEY_SECRET + plainValue: xxx + +App\Entity\Core\AttributeEntity: + attr_entity_transport_type (template): + type: transport_type + workspace: '@w_newspaper' + att_plane (extends attr_entity_transport_type): + value: Plane + translations: + fr: Avion + att_bike (extends attr_entity_transport_type): + value: Bike + translations: + fr: Vélo + att_cat (extends attr_entity_transport_type): + value: Car + translations: + fr: Voiture + att_boat (extends attr_entity_transport_type): + value: Boat + translations: + fr: Bateau + attr_entity_season (template): + type: season + workspace: '@w_newspaper' + att_winter (extends attr_entity_season): + value: Winter + translations: + fr: Hiver + att_spring (extends attr_entity_season): + value: Spring + translations: + fr: Printemps + att_summer (extends attr_entity_season): + value: Summer + translations: + fr: Été + att_autumn (extends attr_entity_season): + value: Autumn + translations: + fr: Automne diff --git a/databox/api/migrations/Version20240702155025.php b/databox/api/migrations/Version20240702155025.php new file mode 100644 index 000000000..5b4aedb82 --- /dev/null +++ b/databox/api/migrations/Version20240702155025.php @@ -0,0 +1,38 @@ +addSql('ALTER TABLE asset_rendition ADD projection BOOLEAN DEFAULT NULL'); + $this->addSql('ALTER TABLE attribute ADD asset_annotations JSON DEFAULT NULL'); + $this->addSql('ALTER TABLE attribute DROP coordinates'); + $this->addSql('ALTER TABLE basket_asset ADD asset_annotations JSON DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('ALTER TABLE asset_rendition DROP projection'); + $this->addSql('ALTER TABLE attribute ADD coordinates TEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE attribute DROP asset_annotations'); + $this->addSql('ALTER TABLE basket_asset DROP asset_annotations'); + } +} diff --git a/databox/api/migrations/Version20240716145148.php b/databox/api/migrations/Version20240716145148.php new file mode 100644 index 000000000..a9910d800 --- /dev/null +++ b/databox/api/migrations/Version20240716145148.php @@ -0,0 +1,48 @@ +addSql('CREATE TABLE attribute_item (id UUID NOT NULL, type VARCHAR(100) NOT NULL, value TEXT NOT NULL, locale VARCHAR(10) DEFAULT NULL, position INT NOT NULL, translations JSON DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX item_type_idx ON attribute_item (type)'); + $this->addSql('COMMENT ON COLUMN attribute_item.id IS \'(DC2Type:uuid)\''); + $this->addSql('COMMENT ON COLUMN attribute_item.created_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN attribute_item.updated_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE attribute DROP CONSTRAINT fk_fa7aeffbb4491b47'); + $this->addSql('DROP INDEX idx_fa7aeffbb4491b47'); + $this->addSql('ALTER TABLE attribute DROP translation_origin_id'); + $this->addSql('ALTER TABLE attribute DROP translation_id'); + $this->addSql('ALTER TABLE attribute DROP translation_origin_hash'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('DROP TABLE attribute_item'); + $this->addSql('ALTER TABLE attribute ADD translation_origin_id UUID DEFAULT NULL'); + $this->addSql('ALTER TABLE attribute ADD translation_id UUID DEFAULT NULL'); + $this->addSql('ALTER TABLE attribute ADD translation_origin_hash VARCHAR(32) DEFAULT NULL'); + $this->addSql('COMMENT ON COLUMN attribute.translation_origin_id IS \'(DC2Type:uuid)\''); + $this->addSql('COMMENT ON COLUMN attribute.translation_id IS \'(DC2Type:uuid)\''); + $this->addSql('ALTER TABLE attribute ADD CONSTRAINT fk_fa7aeffbb4491b47 FOREIGN KEY (translation_origin_id) REFERENCES attribute (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX idx_fa7aeffbb4491b47 ON attribute (translation_origin_id)'); + } +} diff --git a/databox/api/migrations/Version20240716150026.php b/databox/api/migrations/Version20240716150026.php new file mode 100644 index 000000000..c0f13d45d --- /dev/null +++ b/databox/api/migrations/Version20240716150026.php @@ -0,0 +1,32 @@ +addSql('ALTER TABLE attribute_definition ADD entity_type VARCHAR(100) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('ALTER TABLE attribute_definition DROP entity_type'); + } +} diff --git a/databox/api/migrations/Version20240716165827.php b/databox/api/migrations/Version20240716165827.php new file mode 100644 index 000000000..78cc398e0 --- /dev/null +++ b/databox/api/migrations/Version20240716165827.php @@ -0,0 +1,37 @@ +addSql('ALTER TABLE attribute_item ADD workspace_id UUID NOT NULL'); + $this->addSql('COMMENT ON COLUMN attribute_item.workspace_id IS \'(DC2Type:uuid)\''); + $this->addSql('ALTER TABLE attribute_item ADD CONSTRAINT FK_44F3819682D40A1F FOREIGN KEY (workspace_id) REFERENCES workspace (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX IDX_44F3819682D40A1F ON attribute_item (workspace_id)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('ALTER TABLE attribute_item DROP CONSTRAINT FK_44F3819682D40A1F'); + $this->addSql('DROP INDEX IDX_44F3819682D40A1F'); + $this->addSql('ALTER TABLE attribute_item DROP workspace_id'); + } +} diff --git a/databox/api/migrations/Version20240717143101.php b/databox/api/migrations/Version20240717143101.php new file mode 100644 index 000000000..aed1ce9f6 --- /dev/null +++ b/databox/api/migrations/Version20240717143101.php @@ -0,0 +1,50 @@ +addSql('CREATE TABLE attribute_entity (id UUID NOT NULL, workspace_id UUID NOT NULL, type VARCHAR(100) NOT NULL, value TEXT NOT NULL, locale VARCHAR(10) DEFAULT NULL, position INT NOT NULL, translations JSON DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_2CC96A3282D40A1F ON attribute_entity (workspace_id)'); + $this->addSql('CREATE INDEX attr_entity_type_idx ON attribute_entity (type)'); + $this->addSql('COMMENT ON COLUMN attribute_entity.id IS \'(DC2Type:uuid)\''); + $this->addSql('COMMENT ON COLUMN attribute_entity.workspace_id IS \'(DC2Type:uuid)\''); + $this->addSql('COMMENT ON COLUMN attribute_entity.created_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN attribute_entity.updated_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE attribute_entity ADD CONSTRAINT FK_2CC96A3282D40A1F FOREIGN KEY (workspace_id) REFERENCES workspace (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE attribute_item DROP CONSTRAINT fk_44f3819682d40a1f'); + $this->addSql('DROP TABLE attribute_item'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('CREATE TABLE attribute_item (id UUID NOT NULL, workspace_id UUID NOT NULL, type VARCHAR(100) NOT NULL, value TEXT NOT NULL, locale VARCHAR(10) DEFAULT NULL, "position" INT NOT NULL, translations JSON DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX idx_44f3819682d40a1f ON attribute_item (workspace_id)'); + $this->addSql('CREATE INDEX item_type_idx ON attribute_item (type)'); + $this->addSql('COMMENT ON COLUMN attribute_item.id IS \'(DC2Type:uuid)\''); + $this->addSql('COMMENT ON COLUMN attribute_item.workspace_id IS \'(DC2Type:uuid)\''); + $this->addSql('COMMENT ON COLUMN attribute_item.created_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN attribute_item.updated_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE attribute_item ADD CONSTRAINT fk_44f3819682d40a1f FOREIGN KEY (workspace_id) REFERENCES workspace (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE attribute_entity DROP CONSTRAINT FK_2CC96A3282D40A1F'); + $this->addSql('DROP TABLE attribute_entity'); + } +} diff --git a/databox/api/migrations/Version20240730141312.php b/databox/api/migrations/Version20240730141312.php new file mode 100644 index 000000000..7effbd363 --- /dev/null +++ b/databox/api/migrations/Version20240730141312.php @@ -0,0 +1,32 @@ +addSql('ALTER TABLE attribute_entity DROP locale'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('ALTER TABLE attribute_entity ADD locale VARCHAR(10) DEFAULT NULL'); + } +} diff --git a/databox/api/src/Api/Model/Input/Attribute/AbstractExtendedAttributeInput.php b/databox/api/src/Api/Model/Input/Attribute/AbstractExtendedAttributeInput.php index 42a47bb10..83e4b8ecc 100644 --- a/databox/api/src/Api/Model/Input/Attribute/AbstractExtendedAttributeInput.php +++ b/databox/api/src/Api/Model/Input/Attribute/AbstractExtendedAttributeInput.php @@ -20,9 +20,9 @@ abstract class AbstractExtendedAttributeInput extends AbstractBaseAttributeInput public ?string $originVendorContext = null; /** - * @var array|string + * @var array */ - public $coordinates; + public $annotations; /** * "valid" | "review_pending" | "declined". diff --git a/databox/api/src/Api/Model/Input/Attribute/AttributeActionInput.php b/databox/api/src/Api/Model/Input/Attribute/AttributeActionInput.php index a38189d43..b46bfc9d2 100644 --- a/databox/api/src/Api/Model/Input/Attribute/AttributeActionInput.php +++ b/databox/api/src/Api/Model/Input/Attribute/AttributeActionInput.php @@ -11,6 +11,18 @@ class AttributeActionInput extends AbstractExtendedAttributeInput */ public ?string $id = null; + /** + * Attribute IDs. + */ + public ?array $ids = null; + + /** + * Asset IDs. + * + * @var string[] + */ + public ?array $assets = null; + /** * Available actions: * - "set" diff --git a/databox/api/src/Api/Model/Input/Attribute/AttributeBatchUpdateInput.php b/databox/api/src/Api/Model/Input/Attribute/AttributeBatchUpdateInput.php index ec3622750..3d9b35ba8 100644 --- a/databox/api/src/Api/Model/Input/Attribute/AttributeBatchUpdateInput.php +++ b/databox/api/src/Api/Model/Input/Attribute/AttributeBatchUpdateInput.php @@ -4,7 +4,7 @@ namespace App\Api\Model\Input\Attribute; -use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Constraints\NotNull; class AttributeBatchUpdateInput extends AssetAttributeBatchUpdateInput { @@ -13,6 +13,8 @@ class AttributeBatchUpdateInput extends AssetAttributeBatchUpdateInput * * @var string[] */ - #[Assert\NotNull] - public ?array $assets = []; + public ?array $assets = null; + + #[NotNull] + public ?string $workspaceId = null; } diff --git a/databox/api/src/Api/Model/Output/AttributeDefinitionOutput.php b/databox/api/src/Api/Model/Output/AttributeDefinitionOutput.php index 5363b9edc..25f9327ac 100644 --- a/databox/api/src/Api/Model/Output/AttributeDefinitionOutput.php +++ b/databox/api/src/Api/Model/Output/AttributeDefinitionOutput.php @@ -39,6 +39,9 @@ class AttributeDefinitionOutput extends AbstractUuidOutput #[Groups([AttributeDefinition::GROUP_LIST, Asset::GROUP_LIST])] public string $fieldType = TextAttributeType::NAME; + #[Groups([AttributeDefinition::GROUP_LIST])] + public ?string $entityType = null; + #[Groups([AttributeDefinition::GROUP_LIST])] public bool $searchable = true; @@ -48,10 +51,10 @@ class AttributeDefinitionOutput extends AbstractUuidOutput #[Groups([AttributeDefinition::GROUP_LIST])] public bool $facetEnabled = false; - #[Groups([AttributeDefinition::GROUP_LIST])] + #[Groups([AttributeDefinition::GROUP_LIST, Asset::GROUP_LIST])] public bool $translatable = false; - #[Groups([AttributeDefinition::GROUP_LIST])] + #[Groups([AttributeDefinition::GROUP_LIST, Asset::GROUP_LIST])] public bool $multiple = false; #[Groups([AttributeDefinition::GROUP_LIST])] diff --git a/databox/api/src/Api/Model/Output/AttributeOutput.php b/databox/api/src/Api/Model/Output/AttributeOutput.php index 771bd621c..422e39862 100644 --- a/databox/api/src/Api/Model/Output/AttributeOutput.php +++ b/databox/api/src/Api/Model/Output/AttributeOutput.php @@ -37,12 +37,6 @@ class AttributeOutput extends AbstractUuidOutput #[Groups([Asset::GROUP_LIST, Asset::GROUP_READ, Attribute::GROUP_LIST, Attribute::GROUP_READ])] public string|array|null $highlight; - /** - * Unique ID to group translations of the same attribute. - */ - #[Groups([Attribute::GROUP_LIST, Attribute::GROUP_READ, AssetDataTemplate::GROUP_READ])] - public ?string $translationId = null; - /** * "human" or "machine". */ @@ -61,8 +55,8 @@ class AttributeOutput extends AbstractUuidOutput #[Groups([Attribute::GROUP_LIST, Attribute::GROUP_READ])] public ?string $originVendorContext = null; - #[Groups([Attribute::GROUP_LIST, Attribute::GROUP_READ])] - public ?string $coordinates = null; + #[Groups([Attribute::GROUP_LIST, Attribute::GROUP_READ, Asset::GROUP_READ])] + public ?array $assetAnnotations = null; /** * @var string|null @@ -84,10 +78,4 @@ class AttributeOutput extends AbstractUuidOutput #[Groups([Attribute::GROUP_LIST, Attribute::GROUP_READ])] public $confidence; - - /** - * @var bool - */ - #[Groups([Asset::GROUP_LIST, Asset::GROUP_READ, Attribute::GROUP_LIST, Attribute::GROUP_READ])] - public $multiple; } diff --git a/databox/api/src/Api/OutputTransformer/AssetOutputTransformer.php b/databox/api/src/Api/OutputTransformer/AssetOutputTransformer.php index 0c036b5dd..9a9246677 100644 --- a/databox/api/src/Api/OutputTransformer/AssetOutputTransformer.php +++ b/databox/api/src/Api/OutputTransformer/AssetOutputTransformer.php @@ -89,27 +89,16 @@ public function transform(object $data, string $outputClass, array &$context = [ Asset::GROUP_LIST, Asset::GROUP_READ, ], $context)) { - $attributes = $this->attributesResolver->resolveAssetAttributes($data, true); + $attributesIndex = $this->attributesResolver->resolveAssetAttributes($data, true); + $attributes = $attributesIndex->getFlattenAttributes(); if (!empty($highlights)) { $this->attributesResolver->assignHighlight($attributes, $highlights); } - $indexByAttrName = []; - $preferredAttributes = []; - foreach ($attributes as $_attrs) { - foreach ($preferredLocales as $l) { - if (isset($_attrs[$l]) && null !== $_attrs[$l]->getValue()) { - $preferredAttributes[] = $_attrs[$l]; - $key = $this->fieldNameResolver->getFieldNameFromDefinition($_attrs[$l]->getDefinition()); - $indexByAttrName[$key] = $_attrs[$l]->getValue(); - continue 2; - } - } - } - $output->setAttributes($preferredAttributes); + $output->setAttributes($attributes); $output->setTitle($data->getTitle()); - $titleAttribute = $this->assetTitleResolver->resolveTitle($data, $attributes, $preferredLocales); + $titleAttribute = $this->assetTitleResolver->resolveTitle($data, $attributesIndex, $preferredLocales); if ($titleAttribute instanceof Attribute) { $output->setResolvedTitle($titleAttribute->getValue()); $output->setTitleHighlight($titleAttribute->getHighlight()); @@ -122,7 +111,21 @@ public function transform(object $data, string $outputClass, array &$context = [ $groupBy = $context['groupBy'][0] ?? null; if (null !== $groupBy) { - $groupValue = $this->getGroupValue($groupBy, $data, $indexByAttrName[$groupBy] ?? null); + $indexValue = null; + foreach ($attributesIndex->getDefinitions() as $definitionIndex) { + if ($groupBy === $this->fieldNameResolver->getFieldNameFromDefinition($definitionIndex->getDefinition())) { + foreach ($preferredLocales as $l) { + if (null !== $attr = $definitionIndex->getAttribute($l)) { + $indexValue = $attr->getValue(); + break 2; + } + } + + break; + } + } + + $groupValue = $this->getGroupValue($groupBy, $data, $indexValue); $groupKey = $groupValue->getKey(); if ($this->lastGroupKey !== $groupKey) { diff --git a/databox/api/src/Api/OutputTransformer/AttributeDefinitionOutputTransformer.php b/databox/api/src/Api/OutputTransformer/AttributeDefinitionOutputTransformer.php index eb1d025e8..0bcf77b64 100644 --- a/databox/api/src/Api/OutputTransformer/AttributeDefinitionOutputTransformer.php +++ b/databox/api/src/Api/OutputTransformer/AttributeDefinitionOutputTransformer.php @@ -33,6 +33,7 @@ public function transform(object $data, string $outputClass, array &$context = [ $output->slug = $data->getSlug(); $output->fileType = $data->getFileType(); $output->fieldType = $data->getFieldType(); + $output->entityType = $data->getEntityType(); $output->searchable = $data->isSearchable(); $output->suggest = $data->isSuggest(); $output->facetEnabled = $data->isFacetEnabled(); diff --git a/databox/api/src/Api/OutputTransformer/AttributeOutputTransformer.php b/databox/api/src/Api/OutputTransformer/AttributeOutputTransformer.php index 0c4ae0be3..c03626e18 100644 --- a/databox/api/src/Api/OutputTransformer/AttributeOutputTransformer.php +++ b/databox/api/src/Api/OutputTransformer/AttributeOutputTransformer.php @@ -6,9 +6,11 @@ use Alchemy\AuthBundle\Security\Traits\SecurityAwareTrait; use App\Api\Model\Output\AttributeOutput; +use App\Attribute\AttributeInterface; use App\Attribute\AttributeTypeRegistry; use App\Entity\Core\AbstractBaseAttribute; use App\Entity\Core\Attribute; +use App\Entity\Core\AttributeDefinition; class AttributeOutputTransformer implements OutputTransformerInterface { @@ -34,24 +36,24 @@ public function transform(object $data, string $outputClass, array &$context = [ $output->setCreatedAt($data->getCreatedAt()); $output->setUpdatedAt($data->getUpdatedAt()); $output->setId($data->getId()); - $values = $data->getValues(); - $output->value = $values ? array_map(fn (?string $v) => $type->denormalizeValue($v), $data->getValues()) : $type->denormalizeValue($data->getValue()); - $output->multiple = null !== $values; + $output->value = $type->denormalizeValue($data->getValue()); - $output->locale = $data->getLocale(); + /** @var AttributeDefinition $definition */ + $definition = $data->getDefinition(); + $output->locale = $definition->isTranslatable() ? $data->getLocale() : AttributeInterface::NO_LOCALE; $output->position = $data->getPosition(); $output->definition = $data->getDefinition(); if ($data instanceof Attribute) { $output->asset = $data->getAsset(); - $output->highlight = $data->getHighlights() ?? $data->getHighlight(); + $output->highlight = $data->getHighlight(); $output->origin = $data->getOriginLabel(); $output->originUserId = $data->getOriginUserId(); $output->originVendor = $data->getOriginVendor(); $output->originVendorContext = $data->getOriginVendorContext(); $output->status = $data->getStatusLabel(); $output->confidence = $data->getConfidence(); - $output->coordinates = $data->getCoordinates(); + $output->assetAnnotations = $data->getAssetAnnotations(); } return $output; diff --git a/databox/api/src/Api/Processor/AssetAttributeBatchUpdateProcessor.php b/databox/api/src/Api/Processor/AssetAttributeBatchUpdateProcessor.php index dd9f7ada3..5fdbedac5 100644 --- a/databox/api/src/Api/Processor/AssetAttributeBatchUpdateProcessor.php +++ b/databox/api/src/Api/Processor/AssetAttributeBatchUpdateProcessor.php @@ -32,7 +32,7 @@ public function process($data, Operation $operation, array $uriVariables = [], a $asset = DoctrineUtil::findStrict($this->em, Asset::class, $uriVariables['id']); $this->denyAccessUnlessGranted(AssetVoter::EDIT_ATTRIBUTES, $asset); - $this->batchAttributeManager->validate([$asset->getId()], $data); + $this->batchAttributeManager->validate($asset->getWorkspaceId(), [$asset->getId()], $data); $this->batchAttributeManager->handleBatch( $asset->getWorkspaceId(), diff --git a/databox/api/src/Api/Processor/BatchAttributeUpdateProcessor.php b/databox/api/src/Api/Processor/BatchAttributeUpdateProcessor.php index 40df6c70c..560452601 100644 --- a/databox/api/src/Api/Processor/BatchAttributeUpdateProcessor.php +++ b/databox/api/src/Api/Processor/BatchAttributeUpdateProcessor.php @@ -4,21 +4,36 @@ namespace App\Api\Processor; +use Alchemy\AuthBundle\Security\Traits\SecurityAwareTrait; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProcessorInterface; use App\Api\Model\Input\Attribute\AttributeBatchUpdateInput; -use App\Entity\Core\Attribute; +use App\Attribute\BatchAttributeManager; -class BatchAttributeUpdateProcessor implements ProcessorInterface +final class BatchAttributeUpdateProcessor implements ProcessorInterface { + use SecurityAwareTrait; + + public function __construct( + private readonly BatchAttributeManager $batchAttributeManager, + ) { + } + /** * @param AttributeBatchUpdateInput $data */ public function process($data, Operation $operation, array $uriVariables = [], array $context = []) { - $object = new Attribute(); - $object->batchUpdate = $data; + $this->batchAttributeManager->validate($data->workspaceId, $data->assets, $data); + + $this->batchAttributeManager->handleBatch( + $data->workspaceId, + $data->assets, + $data, + $this->getStrictUser(), + true, + ); - return $object; + return null; } } diff --git a/databox/api/src/Api/Provider/AttributeEntityCollectionProvider.php b/databox/api/src/Api/Provider/AttributeEntityCollectionProvider.php new file mode 100644 index 000000000..efb546adb --- /dev/null +++ b/databox/api/src/Api/Provider/AttributeEntityCollectionProvider.php @@ -0,0 +1,45 @@ +entityIriConverter->getItemFromIri(Workspace::class, $workspaceId); + $this->denyAccessUnlessGranted(AbstractVoter::READ, $workspace); + + $queryString = $context['filters']['query'] ?? null; + + if (!empty($queryString)) { + return $this->search->search($workspaceId, $context['filters'] ?? []); + } + + return $this->collectionProvider->provide($operation, $uriVariables, $context); + } +} diff --git a/databox/api/src/Api/Provider/TagCollectionProvider.php b/databox/api/src/Api/Provider/TagCollectionProvider.php new file mode 100644 index 000000000..4fac27cee --- /dev/null +++ b/databox/api/src/Api/Provider/TagCollectionProvider.php @@ -0,0 +1,45 @@ +entityIriConverter->getItemFromIri(Workspace::class, $workspaceId); + $this->denyAccessUnlessGranted(AbstractVoter::READ, $workspace); + + $queryString = $context['filters']['query'] ?? null; + + if (!empty($queryString)) { + return $this->tagSearch->search($workspaceId, $context['filters'] ?? []); + } + + return $this->collectionProvider->provide($operation, $uriVariables, $context); + } +} diff --git a/databox/api/src/Api/Traits/UserLocaleTrait.php b/databox/api/src/Api/Traits/UserLocaleTrait.php index dcfb24504..1f16d93df 100644 --- a/databox/api/src/Api/Traits/UserLocaleTrait.php +++ b/databox/api/src/Api/Traits/UserLocaleTrait.php @@ -2,7 +2,7 @@ namespace App\Api\Traits; -use App\Elasticsearch\Mapping\IndexMappingUpdater; +use App\Attribute\AttributeInterface; use App\Entity\Core\Workspace; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Contracts\Service\Attribute\Required; @@ -25,7 +25,7 @@ protected function getPreferredLocales(Workspace $workspace): array { $userLocales = $this->getUserLocales(); - return array_unique(array_filter(array_merge($userLocales, $workspace->getLocaleFallbacks(), [IndexMappingUpdater::NO_LOCALE]))); + return array_unique(array_filter(array_merge($userLocales, $workspace->getLocaleFallbacks(), [AttributeInterface::NO_LOCALE]))); } #[Required] diff --git a/databox/api/src/Asset/AssetCopier.php b/databox/api/src/Asset/AssetCopier.php index 539afd1b1..528cef1ce 100644 --- a/databox/api/src/Asset/AssetCopier.php +++ b/databox/api/src/Asset/AssetCopier.php @@ -107,7 +107,7 @@ private function doCopyAsset( if ($options[self::OPT_WITH_ATTRIBUTES] ?? false) { $attributes = $this->em->getRepository(Attribute::class) - ->getAssetAttributes($asset); + ->getAssetAttributes($asset->getId()); foreach ($attributes as $attr) { $this->copyAttribute($attr, $copy); @@ -135,7 +135,7 @@ private function copyAttribute(Attribute $attribute, Asset $target): void $copy->setValue($attribute->getValue()); $copy->setConfidence($attribute->getConfidence()); $copy->setCreatedAt($attribute->getCreatedAt()); - $copy->setCoordinates($attribute->getCoordinates()); + $copy->setAssetAnnotations($attribute->getAssetAnnotations()); $copy->setLocale($attribute->getLocale()); $copy->setOriginUserId($attribute->getOriginUserId()); $copy->setOriginVendor($attribute->getOriginVendor()); diff --git a/databox/api/src/Asset/Attribute/AssetTitleResolver.php b/databox/api/src/Asset/Attribute/AssetTitleResolver.php index acd99bc4a..d34fa5fae 100644 --- a/databox/api/src/Asset/Attribute/AssetTitleResolver.php +++ b/databox/api/src/Asset/Attribute/AssetTitleResolver.php @@ -4,6 +4,7 @@ namespace App\Asset\Attribute; +use App\Asset\Attribute\Index\AttributeIndex; use App\Entity\Core\Asset; use App\Entity\Core\AssetTitleAttribute; use App\Entity\Core\Attribute; @@ -17,22 +18,16 @@ public function __construct(private readonly EntityManagerInterface $em) { } - /** - * @param array> $attributes - */ - public function resolveTitle(Asset $asset, array $attributes, array $preferredLocales): Attribute|string|null + public function resolveTitle(Asset $asset, AttributeIndex $attributesIndex, array $preferredLocales): Attribute|string|null { if (empty($asset->getTitle()) || $this->hasTitleOverride($asset->getWorkspaceId())) { $titleAttrs = $this->getTitleAttributes($asset->getWorkspaceId()); foreach ($titleAttrs as $attrTitle) { - foreach ($attributes as $_attrs) { - foreach ($preferredLocales as $l) { - if (isset($_attrs[$l])) { - $attribute = $_attrs[$l]; - if ($attribute->getDefinition()->getId() === $attrTitle->getDefinition()->getId()) { - return $attribute; - } - } + $definitionId = $attrTitle->getDefinition()->getId(); + + foreach ($preferredLocales as $l) { + if (null !== $attribute = $attributesIndex->getAttribute($definitionId, $l)) { + return $attribute; } } } diff --git a/databox/api/src/Asset/Attribute/AttributesResolver.php b/databox/api/src/Asset/Attribute/AttributesResolver.php index 10b0dc918..e6ec5c352 100644 --- a/databox/api/src/Asset/Attribute/AttributesResolver.php +++ b/databox/api/src/Asset/Attribute/AttributesResolver.php @@ -4,13 +4,13 @@ namespace App\Asset\Attribute; +use App\Asset\Attribute\Index\AttributeIndex; +use App\Attribute\AttributeInterface; use App\Elasticsearch\Mapping\FieldNameResolver; -use App\Elasticsearch\Mapping\IndexMappingUpdater; -use App\Entity\Core\AbstractBaseAttribute; use App\Entity\Core\Asset; use App\Entity\Core\Attribute; use App\Entity\Core\AttributeDefinition; -use App\Security\Voter\AbstractVoter; +use App\Security\Voter\AttributeDefinitionVoter; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\SecurityBundle\Security; @@ -20,80 +20,45 @@ public function __construct( private EntityManagerInterface $em, private FieldNameResolver $fieldNameResolver, private FallbackResolver $fallbackResolver, - private Security $security + private Security $security, ) { } - /** - * @return array> - */ - public function resolveAssetAttributes(Asset $asset, bool $applyPermissions): array + public function resolveAssetAttributes(Asset $asset, bool $applyPermissions): AttributeIndex { /** @var Attribute[] $attributes */ $attributes = $this->em->getRepository(Attribute::class) - ->getAssetAttributes($asset); + ->getAssetAttributes($asset->getId()); - $groupedByDef = $this->groupAttributesByLocale($attributes, $applyPermissions); + $index = $this->buildIndex($attributes); + $this->resolveFallbacks($asset, $index); + + if ($applyPermissions) { + foreach ($index->getDefinitions() as $definitionIndex) { + $definition = $definitionIndex->getDefinition(); + if (!$this->security->isGranted(AttributeDefinitionVoter::VIEW_ATTRIBUTES, $definition)) { + $index->removeDefinition($definition->getId()); + } + } + } - return $this->resolveFallbacks($asset, $groupedByDef); + return $index; } /** - * @param AbstractBaseAttribute[] $attributes - * - * @return array> + * @param Attribute[] $attributes */ - public function groupAttributesByLocale(iterable $attributes, bool $applyPermissions): array + private function buildIndex(array $attributes): AttributeIndex { - $disallowedDefinitions = []; - - /** @var array> $groupedByDef */ - $groupedByDef = []; + $index = new AttributeIndex(); foreach ($attributes as $attribute) { - $def = $attribute->getDefinition(); - $k = $def->getId(); - $locale = $attribute->getLocale() ?? IndexMappingUpdater::NO_LOCALE; - - if (!isset($groupedByDef[$k][$locale])) { - if (!isset($groupedByDef[$k])) { - $groupedByDef[$k] = []; - } - - $groupedByDef[$k][$locale] = clone $attribute; - $attribute->setValues(null); // Reset values aggregation - - if ($applyPermissions - && !isset($disallowedDefinitions[$k]) - ) { - assert($attribute instanceof Attribute); - $disallowedDefinitions[$k] = !$this->security->isGranted(AbstractVoter::READ, $attribute); - } - } - - $groupAttr = $groupedByDef[$k][$locale]; - - if ($def->isMultiple()) { - $values = $groupAttr->getValues() ?? []; - $values[] = $attribute->getValue(); - $groupAttr->setValues($values); - } + $index->addAttribute($attribute); } - unset($attributes); - if ($applyPermissions) { - $disallowedDefinitions = array_filter($disallowedDefinitions, fn (bool $v): bool => $v); - $groupedByDef = array_diff_key($groupedByDef, $disallowedDefinitions); - } - - return $groupedByDef; + return $index; } - /** - * @param array> $attributes - * - * @return array> - */ - private function resolveFallbacks(Asset $asset, array $attributes): array + private function resolveFallbacks(Asset $asset, AttributeIndex $attributes): void { /** @var AttributeDefinition[] $fbDefinitions */ $fbDefinitions = $this->em @@ -106,7 +71,7 @@ private function resolveFallbacks(Asset $asset, array $attributes): array $fallbacks = $definition->getFallback(); if (null !== $fallbacks) { foreach ($fallbacks as $locale => $fb) { - if (!isset($attributes[$k][$locale])) { + if (null === $attributes->getAttribute($k, $locale)) { $attr = $this->fallbackResolver->resolveAttrFallback( $asset, $locale, @@ -114,47 +79,37 @@ private function resolveFallbacks(Asset $asset, array $attributes): array $attributes ); if (null !== $attr) { - $attributes[$k][$locale] = $attr; + $attributes->addAttribute($attr); } } } } } - - return $attributes; } + /** + * @param Attribute[] $attributes + */ public function assignHighlight(array $attributes, array $highlights): void { - foreach ($attributes as $_attrs) { - foreach ($_attrs as $locale => $attribute) { - $f = $this->fieldNameResolver->getFieldNameFromDefinition($attribute->getDefinition()); - - $fieldName = sprintf('attributes.%s.%s', $locale, $f); - - if ($h = ($highlights[$fieldName] ?? null)) { - if ($attribute->getDefinition()->isMultiple()) { - $values = $attribute->getValues(); - $newValues = []; - - foreach ($values as $v) { - $found = false; - foreach ($highlights[$fieldName] as $hlValue) { - if (preg_replace('#\[hl](.*)\[/hl]#', '$1', (string) $hlValue) === $v) { - $found = true; - $newValues[] = $hlValue; - break; - } - } - if (!$found) { - $newValues[] = $v; - } + foreach ($attributes as $attribute) { + $locale = $attribute->getLocale() ?? AttributeInterface::NO_LOCALE; + $definition = $attribute->getDefinition(); + $f = $this->fieldNameResolver->getFieldNameFromDefinition($definition); + + $fieldName = sprintf('attributes.%s.%s', $locale, $f); + + if ($h = ($highlights[$fieldName] ?? null)) { + if ($definition->isMultiple()) { + $v = $attribute->getValue(); + foreach ($h as $hlValue) { + if (preg_replace('#\[hl](.*)\[/hl]#', '$1', (string) $hlValue) === $v) { + $attribute->setHighlight($hlValue); + break; } - - $attribute->setHighlights($newValues); - } else { - $attribute->setHighlight(reset($h)); } + } else { + $attribute->setHighlight(reset($h)); } } } diff --git a/databox/api/src/Asset/Attribute/DynamicAttributeBag.php b/databox/api/src/Asset/Attribute/DynamicAttributeBag.php index 41b0fce3e..f0c249d97 100644 --- a/databox/api/src/Asset/Attribute/DynamicAttributeBag.php +++ b/databox/api/src/Asset/Attribute/DynamicAttributeBag.php @@ -4,25 +4,27 @@ namespace App\Asset\Attribute; -use App\Elasticsearch\Mapping\IndexMappingUpdater; +use App\Asset\Attribute\Index\AttributeIndex; +use App\Attribute\AttributeInterface; use App\Entity\Core\Attribute; use App\Entity\Core\AttributeDefinition; class DynamicAttributeBag { private $resolve; + private readonly array $locales; /** - * @param array $attributes * @param array $definitions */ public function __construct( - private readonly array $attributes, + private readonly AttributeIndex $attributes, private readonly array $definitions, callable $resolve, - private readonly string $locale + string $locale ) { $this->resolve = $resolve; + $this->locales = array_unique([$locale, AttributeInterface::NO_LOCALE]); } public function __call(string $name, $args): ?string @@ -34,9 +36,9 @@ public function __call(string $name, $args): ?string $defId = $def->getId(); - foreach ([$this->locale, IndexMappingUpdater::NO_LOCALE] as $l) { - if (isset($this->attributes[$defId][$l])) { - return $this->attributes[$defId][$l]->getValue(); + foreach ($this->locales as $l) { + if (null !== $attr = $this->attributes->getAttribute($defId, $l)) { + return $attr->getValue(); } } diff --git a/databox/api/src/Asset/Attribute/FallbackResolver.php b/databox/api/src/Asset/Attribute/FallbackResolver.php index 0ab6c35ac..09977cc6c 100644 --- a/databox/api/src/Asset/Attribute/FallbackResolver.php +++ b/databox/api/src/Asset/Attribute/FallbackResolver.php @@ -4,6 +4,7 @@ namespace App\Asset\Attribute; +use App\Asset\Attribute\Index\AttributeIndex; use App\Entity\Core\Asset; use App\Entity\Core\Attribute; use App\Entity\Core\AttributeDefinition; @@ -39,33 +40,30 @@ private function getDefinitionIndexByName(string $workspaceId): array return $this->indexByName; } - /** - * @param array> $attributes - */ public function resolveAttrFallback( Asset $asset, string $locale, AttributeDefinition $definition, - array &$attributes + AttributeIndex $attributesIndex ): ?Attribute { $definitionsIndex = $this->getDefinitionIndexByName($asset->getWorkspaceId()); $fallbacks = $definition->getFallback(); if (!empty($fallbacks[$locale])) { - if (!isset($attributes[$definition->getId()][$locale])) { + if (null === $attributesIndex->getAttribute($definition->getId(), $locale)) { $fallbackValue = $this->resolveFallback($fallbacks[$locale], [ 'file' => $asset->getSource(), 'asset' => $asset, - 'attr' => new DynamicAttributeBag($attributes, $definitionsIndex, function (AttributeDefinition $depDef) use ( + 'attr' => new DynamicAttributeBag($attributesIndex, $definitionsIndex, function (AttributeDefinition $depDef) use ( $asset, - &$attributes, + $attributesIndex, $locale ): ?Attribute { return $this->resolveAttrFallback( $asset, $locale, $depDef, - $attributes + $attributesIndex ); }, $locale), ]); @@ -79,11 +77,7 @@ public function resolveAttrFallback( $attribute->setOrigin(Attribute::ORIGIN_FALLBACK); $attribute->setValue($fallbackValue); - if ($definition->isMultiple()) { - $attribute->setValues([$fallbackValue]); - } - - $attributes[$definition->getId()][$locale] = $attribute; + $attributesIndex->addAttribute($attribute); return $attribute; } diff --git a/databox/api/src/Asset/Attribute/Index/AttributeIndex.php b/databox/api/src/Asset/Attribute/Index/AttributeIndex.php new file mode 100644 index 000000000..492b3705d --- /dev/null +++ b/databox/api/src/Asset/Attribute/Index/AttributeIndex.php @@ -0,0 +1,49 @@ +getDefinition(); + $definitionId = $definition->getId(); + $this->definitions[$definitionId] ??= new DefinitionIndex($definition); + $this->definitions[$definitionId]->addAttribute($attribute); + } + + public function removeDefinition(string $definitionId): void + { + unset($this->definitions[$definitionId]); + } + + public function getAttribute(string $definitionId, string $locale): ?Attribute + { + return $this->definitions[$definitionId]?->getAttribute($locale); + } + + /** + * @return DefinitionIndex[] + */ + public function getDefinitions(): array + { + return $this->definitions; + } + + /** + * @return Attribute[] + */ + public function getFlattenAttributes(): array + { + $arrays = array_values(array_map(fn (DefinitionIndex $definitionIndex): array => $definitionIndex->getFlattenAttributes(), $this->definitions)); + + return array_merge(...$arrays); + } +} diff --git a/databox/api/src/Asset/Attribute/Index/DefinitionIndex.php b/databox/api/src/Asset/Attribute/Index/DefinitionIndex.php new file mode 100644 index 000000000..478fef062 --- /dev/null +++ b/databox/api/src/Asset/Attribute/Index/DefinitionIndex.php @@ -0,0 +1,54 @@ +locales; + } + + public function getDefinition(): AttributeDefinition + { + return $this->definition; + } + + public function addAttribute(Attribute $attribute): void + { + $locale = $attribute->getLocale() ?? AttributeInterface::NO_LOCALE; + + if ($this->definition->isMultiple()) { + $this->locales[$locale] ??= []; + $this->locales[$locale][] = $attribute; + } else { + $this->locales[$locale] = $attribute; + } + } + + public function getAttribute(string $locale): ?Attribute + { + return $this->locales[$locale] ?? null; + } + + /** + * @return Attribute[] + */ + public function getFlattenAttributes(): array + { + return array_merge(...array_values(array_map(fn (array|Attribute $value): array => $value instanceof Attribute ? [$value] : $value, $this->locales))); + } +} diff --git a/databox/api/src/Asset/Attribute/InitialAttributeValuesResolver.php b/databox/api/src/Asset/Attribute/InitialAttributeValuesResolver.php index 667292948..ecd76ed65 100644 --- a/databox/api/src/Asset/Attribute/InitialAttributeValuesResolver.php +++ b/databox/api/src/Asset/Attribute/InitialAttributeValuesResolver.php @@ -11,7 +11,7 @@ use App\Entity\Core\Attribute; use App\Entity\Core\AttributeDefinition; use App\File\FileMetadataAccessorWrapper; -use App\Repository\Core\AttributeDefinitionRepositoryInterface; +use App\Repository\Core\AttributeDefinitionRepository; use Doctrine\ORM\EntityManagerInterface; use Twig\Environment; use Twig\Loader\ArrayLoader; @@ -36,7 +36,7 @@ public function resolveInitialAttributes(Asset $asset): array { $attributes = []; - /** @var AttributeDefinitionRepositoryInterface $repo */ + /** @var AttributeDefinitionRepository $repo */ $repo = $this->em->getRepository(AttributeDefinition::class); $definitions = $repo->getWorkspaceInitializeDefinitions($asset->getWorkspaceId()); diff --git a/databox/api/src/Attribute/AttributeAssigner.php b/databox/api/src/Attribute/AttributeAssigner.php index 6a80af46e..af2cd03e9 100644 --- a/databox/api/src/Attribute/AttributeAssigner.php +++ b/databox/api/src/Attribute/AttributeAssigner.php @@ -43,7 +43,7 @@ public function assignAttributeFromInput(AbstractBaseAttribute $attribute, Abstr if ($data->confidence) { $attribute->setConfidence($data->confidence); } - $attribute->setCoordinates($data->coordinates); + $attribute->setAssetAnnotations($data->annotations); } if ($data->locale) { diff --git a/databox/api/src/Attribute/AttributeInterface.php b/databox/api/src/Attribute/AttributeInterface.php new file mode 100644 index 000000000..4ec7dc5a3 --- /dev/null +++ b/databox/api/src/Attribute/AttributeInterface.php @@ -0,0 +1,9 @@ +setLocale($attribute->getLocale()); $a->setAsset($attribute->getAsset()); $a->setConfidence($attribute->getConfidence()); - $a->setCoordinates($attribute->getCoordinates()); + $a->setAssetAnnotations($attribute->getAssetAnnotations()); $a->setOrigin($attribute->getOrigin()); $a->setStatus($attribute->getStatus()); $a->setOriginUserId($attribute->getOriginUserId()); $a->setOriginVendor($attribute->getOriginVendor()); $a->setOriginVendorContext($attribute->getOriginVendorContext()); - $a->setTranslationId($attribute->getTranslationId()); $a->setValue($p); $this->em->persist($a); diff --git a/databox/api/src/Attribute/BatchAttributeManager.php b/databox/api/src/Attribute/BatchAttributeManager.php index e7b01dff4..b67e8dcd8 100644 --- a/databox/api/src/Attribute/BatchAttributeManager.php +++ b/databox/api/src/Attribute/BatchAttributeManager.php @@ -9,6 +9,7 @@ use Alchemy\AuthBundle\Security\JwtUser; use Alchemy\ESBundle\Listener\DeferredIndexListener; use Alchemy\MessengerBundle\Listener\PostFlushStack; +use ApiPlatform\Validator\Exception\ValidationException; use App\Api\Model\Input\Attribute\AssetAttributeBatchUpdateInput; use App\Api\Model\Input\Attribute\AttributeActionInput; use App\Consumer\Handler\Asset\AttributeChanged; @@ -16,12 +17,17 @@ use App\Entity\Core\Attribute; use App\Entity\Core\AttributeDefinition; use App\Security\Voter\AssetVoter; +use Doctrine\DBAL\ArrayParameterType; +use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Types\ConversionException; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\QueryBuilder; use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\Validator\Context\ExecutionContext; +use Symfony\Component\Validator\Validator\ValidatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; class BatchAttributeManager { @@ -37,34 +43,45 @@ public function __construct( private readonly PostFlushStack $postFlushStack, private readonly AttributeManager $attributeManager, private readonly DeferredIndexListener $deferredIndexListener, + private readonly AttributeTypeRegistry $typeRegistry, + private readonly ValidatorInterface $validator, + private readonly TranslatorInterface $translator, ) { } - public function validate(array $assetsId, AssetAttributeBatchUpdateInput $input): ?string + public function validate(string $workspaceId, ?array $assetsId, AssetAttributeBatchUpdateInput $input): void { - if (empty($assetsId)) { - return null; - } + $validationContext = new ExecutionContext( + $this->validator, + 'actions', + $this->translator, + ); - $firstId = $assetsId[0]; - /** @var Asset $assetOne */ - $assetOne = $this->em->getRepository(Asset::class)->find($firstId); - if (!$assetOne instanceof Asset) { - throw new \InvalidArgumentException(sprintf('Asset "%s" not found', $firstId)); + $allAssetIndex = []; + foreach (($assetsId ?? []) as $id) { + $allAssetIndex[$id] = true; } + foreach ($input->actions as $action) { + if (null !== $action->assets) { + foreach ($action->assets as $id) { + $allAssetIndex[$id] = true; + } + } + } + + $allAssetIds = array_keys($allAssetIndex); - $workspaceId = $assetOne->getWorkspaceId(); $assets = $this->em->createQueryBuilder() ->select('a') ->from(Asset::class, 'a') ->andWhere('a.workspace = :w') ->setParameter('w', $workspaceId) ->andWhere('a.id IN (:ids)') - ->setParameter('ids', $assetsId) + ->setParameter('ids', $allAssetIds) ->getQuery() ->getResult(); - if (count($assets) !== count($assetsId)) { + if (count($assets) !== count($allAssetIds)) { throw new \InvalidArgumentException('Some assets where not found. Possible issues: there are coming from different workspaces, they were deleted'); } @@ -76,8 +93,16 @@ public function validate(array $assetsId, AssetAttributeBatchUpdateInput $input) foreach ($input->actions as $i => $action) { if ($action->definitionId) { - $definition = $this->getAttributeDefinition($workspaceId, $action->definitionId); - $this->denyUnlessGranted($definition); + if ('tags' !== $action->definitionId) { + $definition = $this->getAttributeDefinition($workspaceId, $action->definitionId); + + if ($action->value) { + $type = $this->typeRegistry->getStrictType($definition->getFieldType()); + $type->validate($action->value, $validationContext); + } + + $this->denyUnlessGranted($definition); + } } elseif ($action->name) { $definition = $this->getAttributeDefinitionBySlug($workspaceId, $action->name); $this->denyUnlessGranted($definition); @@ -96,7 +121,9 @@ public function validate(array $assetsId, AssetAttributeBatchUpdateInput $input) } } - return $workspaceId; + if ($validationContext->getViolations()->count() > 0) { + throw new ValidationException($validationContext->getViolations()); + } } private function denyUnlessGranted(AttributeDefinition $definition): void @@ -109,23 +136,37 @@ private function denyUnlessGranted(AttributeDefinition $definition): void public function handleBatch( string $workspaceId, - array $assetsId, + ?array $assetsId, AssetAttributeBatchUpdateInput $input, ?JwtUser $user, bool $dispatchUpdateEvent = false, ): void { - if (empty($assetsId)) { - return; - } - DeferredIndexListener::disable(); + $assetsId ??= []; try { $this->em->wrapInTransaction(function () use ($user, $input, $assetsId, $workspaceId, $dispatchUpdateEvent): void { + $updatedAssets = []; $changedAttributeDefinitions = []; + foreach ($assetsId as $id) { + $updatedAssets[$id] = true; + } + foreach ($input->actions as $i => $action) { + $ids = $action->assets ?? $assetsId; + if (null !== $action->assets) { + foreach ($action->assets as $id) { + $updatedAssets[$id] = true; + } + } + if ($action->definitionId) { + if ('tags' === $action->definitionId) { + $this->handleTagAction($action, $ids); + + continue; + } $definition = $this->getAttributeDefinition($workspaceId, $action->definitionId); } elseif ($action->name) { $definition = $this->getAttributeDefinitionBySlug($workspaceId, $action->name); @@ -148,14 +189,15 @@ public function handleBatch( throw new BadRequestHttpException(sprintf('Attribute "%s" is not multi-valued in action #%d', $definition->getName(), $i)); } - $this->upsertAttribute(null, $assetsId, $definition, $action); + $this->upsertAttribute(null, $ids, $definition, $action); break; case self::ACTION_DELETE: if (!$definition) { throw new BadRequestHttpException(sprintf('Missing definitionId in action #%d', $i)); } - $this->deleteAttributes($assetsId, $definition, $user, [ + $this->deleteAttributes($ids, $definition, $user, [ 'id' => $action->id, + 'ids' => $action->ids, 'origin' => $action->origin, 'originVendor' => $action->originVendor, ]); @@ -167,7 +209,7 @@ public function handleBatch( if (!$attribute instanceof Attribute) { throw new BadRequestHttpException(sprintf('Attribute "%s" not found in action #%d', $action->id, $i)); } - $this->upsertAttribute($attribute, $assetsId, $definition, $action); + $this->upsertAttribute($attribute, $ids, $definition, $action); } catch (ConversionException $e) { throw new BadRequestHttpException(sprintf('Invalid attribute ID "%s" in action #%d', $action->id, $i), $e); } @@ -180,14 +222,14 @@ public function handleBatch( throw new BadRequestHttpException(sprintf('Attribute "%s" is a multi-valued in action #%d, use add/delete actions for this kind of attribute or pass an array in "value"', $definition->getName(), $i)); } - $this->deleteAttributes($assetsId, $definition, $user); + $this->deleteAttributes($ids, $definition, $user); foreach ($action->value as $value) { $vAction = clone $action; $vAction->value = $value; - $this->upsertAttribute(null, $assetsId, $definition, $vAction); + $this->upsertAttribute(null, $ids, $definition, $vAction); } } else { - foreach ($assetsId as $assetId) { + foreach ($ids as $assetId) { $attribute = $this->em->getRepository(Attribute::class)->findOneBy([ 'definition' => $definition->getId(), 'asset' => $assetId, @@ -215,7 +257,7 @@ public function handleBatch( $qb ->from(Attribute::class, 'a') ->andWhere('a.asset IN (:assets)') - ->setParameter('assets', $assetsId) + ->setParameter('assets', $ids) ->setParameter('from', $action->value) ->setParameter('to', $action->replaceWith); if ($definition) { @@ -254,18 +296,19 @@ public function handleBatch( } } + $updatedAssetIds = array_keys($updatedAssets); $this->em->createQueryBuilder() ->update() ->from(Asset::class, 't') ->set('t.attributesEditedAt', ':now') ->andWhere('t.id IN (:ids)') ->setParameter('now', new \DateTimeImmutable()) - ->setParameter('ids', $assetsId) + ->setParameter('ids', $updatedAssetIds) ->getQuery() ->execute(); $attributes = array_keys($changedAttributeDefinitions); - foreach ($assetsId as $assetId) { + foreach ($updatedAssetIds as $assetId) { // Force assets to be re-indexed $this->deferredIndexListener->scheduleForUpdate($this->em->getReference(Asset::class, $assetId)); @@ -285,6 +328,49 @@ public function handleBatch( } } + private function handleTagAction(AttributeActionInput $action, array $assetIds): void + { + $assetMeta = $this->em->getClassMetadata(Asset::class); + $tagMapping = $assetMeta->getAssociationMapping('tags'); + $joinTable = $tagMapping['joinTable']; + $assetTable = $assetMeta->getTableName(); + $tagAssociationTable = $joinTable['name']; + $assetIdCol = $joinTable['joinColumns'][0]['name']; + $tagIdCol = $joinTable['inverseJoinColumns'][0]['name']; + + switch ($action->action) { + case self::ACTION_ADD: + $query = sprintf( + 'INSERT INTO %1$s (%2$s, %3$s) SELECT :tag, a.id FROM %4$s a WHERE a.id IN (:ids) ON CONFLICT DO NOTHING', + $tagAssociationTable, + $tagIdCol, + $assetIdCol, + $assetTable, + ); + $this->em->getConnection()->executeQuery($query, [ + 'tag' => $action->value, + 'ids' => $assetIds, + ], [ + 'tag' => ParameterType::STRING, + 'ids' => ArrayParameterType::STRING, + ]); + break; + case self::ACTION_DELETE: + $this->em->getConnection()->executeQuery(sprintf( + 'DELETE FROM %1$s WHERE %3$s IN (:ids) AND %2$s IN (:tags)', + $tagAssociationTable, + $tagIdCol, + $assetIdCol, + ), [ + 'tags' => $action->ids, + 'ids' => $assetIds, + ], [ + 'tags' => ArrayParameterType::STRING, + 'ids' => ArrayParameterType::STRING, + ]); + } + } + private function upsertAttribute( ?Attribute $attribute, array $assetsId, @@ -361,6 +447,11 @@ private function deleteAttributes( ->andWhere('a.id = :id') ->setParameter('id', $options['id']); } + if ($options['ids'] ?? null) { + $qb + ->andWhere('a.id IN (:ids)') + ->setParameter('ids', $options['ids']); + } if ($options['origin'] ?? null) { $qb ->andWhere('a.origin = :origin') diff --git a/databox/api/src/Attribute/Type/AbstractAttributeType.php b/databox/api/src/Attribute/Type/AbstractAttributeType.php index c06f3c8a6..23665d541 100644 --- a/databox/api/src/Attribute/Type/AbstractAttributeType.php +++ b/databox/api/src/Attribute/Type/AbstractAttributeType.php @@ -58,6 +58,11 @@ public function supportsSuggest(): bool return false; } + public function supportsTranslations(): bool + { + return false; + } + public function getGroupValueLabel($value): ?string { if (null === $value) { diff --git a/databox/api/src/Attribute/Type/AttributeTypeInterface.php b/databox/api/src/Attribute/Type/AttributeTypeInterface.php index f83e1ed6a..cd3ac81ef 100644 --- a/databox/api/src/Attribute/Type/AttributeTypeInterface.php +++ b/databox/api/src/Attribute/Type/AttributeTypeInterface.php @@ -18,8 +18,11 @@ interface AttributeTypeInterface public static function getName(): string; public function getElasticSearchType(): string; + public function getElasticSearchSubField(): ?string; + public function getElasticSearchSearchType(): ?SearchType; + public function supportsElasticSearchFuzziness(): bool; public function getFacetType(): string; @@ -59,6 +62,8 @@ public function isLocaleAware(): bool; public function supportsSuggest(): bool; + public function supportsTranslations(): bool; + public function validate($value, ExecutionContextInterface $context): void; public function getAggregationField(): ?string; diff --git a/databox/api/src/Attribute/Type/EntityAttributeType.php b/databox/api/src/Attribute/Type/EntityAttributeType.php new file mode 100644 index 000000000..7d57ca7e6 --- /dev/null +++ b/databox/api/src/Attribute/Type/EntityAttributeType.php @@ -0,0 +1,129 @@ +addViolation('Invalid entity ID'); + } + } + + public function normalizeElasticsearchValue(?string $value): string|array|null + { + $entity = $this->getEntityFromValue($value); + if ($entity instanceof AttributeEntity) { + $locales = array_merge($entity->getTranslations() ?? [], [ + AttributeInterface::NO_LOCALE => $entity->getValue(), + ]); + $entityId = $entity->getId(); + + return array_filter(array_map(function ($v) use ($entityId): ?array { + if (empty($v)) { + return null; + } + + return [ + 'id' => $entityId, + 'value' => $v, + ]; + }, $locales)); + } + + return null; + } + + public function getElasticSearchSubField(): ?string + { + return 'value'; + } + + public function normalizeValue($value): ?string + { + if (null === $value) { + return null; + } + + if ($value instanceof AttributeEntity) { + return $value->getId(); + } + + return $value; + } + + private function getEntityFromValue(?string $value): ?AttributeEntity + { + if (null === $value) { + return null; + } + + return $this->repository->find($value); + } + + public function denormalizeValue(?string $value): ?array + { + $entity = $this->getEntityFromValue($value); + if (!$entity instanceof AttributeEntity) { + return null; + } + + $id = $entity->getId(); + $v = $entity->getValue(); + + return [ + 'id' => $id, + 'value' => $v, + 'createdAt' => $entity->getCreatedAt(), + ]; + } + + public function getElasticSearchMapping(string $locale, AttributeDefinition $definition): array + { + $mapping = parent::getElasticSearchMapping($locale, $definition); + + return [ + 'type' => 'object', + 'properties' => [ + 'value' => [ + ...$mapping, + 'type' => $this->getElasticSearchType(), + ], + 'id' => [ + 'type' => 'keyword', + ], + ], + ]; + } +} diff --git a/databox/api/src/Consumer/Handler/Search/AttributeEntityDelete.php b/databox/api/src/Consumer/Handler/Search/AttributeEntityDelete.php new file mode 100644 index 000000000..ba2059972 --- /dev/null +++ b/databox/api/src/Consumer/Handler/Search/AttributeEntityDelete.php @@ -0,0 +1,31 @@ +id; + } + + public function getType(): string + { + return $this->type; + } + + public function getWorkspaceId(): string + { + return $this->wId; + } +} diff --git a/databox/api/src/Consumer/Handler/Search/AttributeEntityDeleteHandler.php b/databox/api/src/Consumer/Handler/Search/AttributeEntityDeleteHandler.php new file mode 100644 index 000000000..84b2610c8 --- /dev/null +++ b/databox/api/src/Consumer/Handler/Search/AttributeEntityDeleteHandler.php @@ -0,0 +1,95 @@ +getId(); + + $definitions = $this->attributeDefinitionRepository->getWorkspaceDefinitionOfEntity( + $message->getWorkspaceId(), + $message->getType(), + ); + + if (empty($definitions)) { + return; + } + + $fields = []; + $calls = []; + $params = []; + foreach ($definitions as $definition) { + $fieldName = $this->fieldNameResolver->getFieldNameFromDefinition($definition); + $fields[sprintf('%s.%s.%s', AttributeInterface::ATTRIBUTES_FIELD, AttributeInterface::NO_LOCALE, $fieldName)] = true; + $calls[] = sprintf( + 'del(ctx._source.%2$s, \'%1$s\', params[\'_id\']);', + $fieldName, + AttributeInterface::ATTRIBUTES_FIELD + ); + } + + $this->attributeRepository->deleteByAttributeEntity( + $message->getId(), + $message->getWorkspaceId(), + $message->getType() + ); + + $this->elasticSearchClient->updateByQuery( + 'asset', + [ + 'bool' => [ + 'should' => array_map(function (string $field) use ($id): array { + return [ + 'term' => [ + $field.'.id' => $id, + ], + ]; + }, array_keys($fields)), + ], + ], + [ + 'source' => << item['id'] == id); + } else if (field instanceof Map) { + c[locale].remove(name); + } + } + } +} + +EOF.implode("\n", $calls), + 'params' => array_merge($params, [ + '_id' => $id, + ]), + 'lang' => 'painless', + ] + ); + } +} diff --git a/databox/api/src/Consumer/Handler/Search/AttributeEntityUpdate.php b/databox/api/src/Consumer/Handler/Search/AttributeEntityUpdate.php new file mode 100644 index 000000000..b1a35c08f --- /dev/null +++ b/databox/api/src/Consumer/Handler/Search/AttributeEntityUpdate.php @@ -0,0 +1,25 @@ +id; + } + + public function getChanges(): array + { + return $this->changes; + } +} diff --git a/databox/api/src/Consumer/Handler/Search/AttributeEntityUpdateHandler.php b/databox/api/src/Consumer/Handler/Search/AttributeEntityUpdateHandler.php new file mode 100644 index 000000000..20790fc1a --- /dev/null +++ b/databox/api/src/Consumer/Handler/Search/AttributeEntityUpdateHandler.php @@ -0,0 +1,136 @@ +getId(); + $attributeEntity = DoctrineUtil::findStrictByRepo($this->attributeEntityRepository, $id); + $definitions = $this->attributeDefinitionRepository->getWorkspaceDefinitionOfEntity( + $attributeEntity->getWorkspaceId(), + $attributeEntity->getType(), + ); + + $fields = []; + $calls = []; + $params = []; + foreach ($definitions as $definition) { + $fieldName = $this->fieldNameResolver->getFieldNameFromDefinition($definition); + foreach ($message->getChanges() as $locale => $change) { + $fields[sprintf('%s.%s.%s', AttributeInterface::ATTRIBUTES_FIELD, AttributeInterface::NO_LOCALE, $fieldName)] = true; + $params[$locale] = $change; + $calls[$locale] = sprintf( + 'up(ctx._source.%3$s, \'%1$s\', \'%2$s\', params[\'_id\'], params[\'%1$s\'], %4$s);', + $locale, + $fieldName, + AttributeInterface::ATTRIBUTES_FIELD, + $definition->isMultiple() ? 'true' : 'false', + ); + } + } + + $this->elasticSearchClient->updateByQuery( + 'asset', + [ + 'bool' => [ + 'should' => array_map(function (string $field) use ($id): array { + return [ + 'term' => [ + $field.'.id' => $id, + ], + ]; + }, array_keys($fields)), + ], + ], + [ + 'source' => << array_merge($params, [ + '_id' => $id, + ]), + 'lang' => 'painless', + ] + ); + } +} diff --git a/databox/api/src/Consumer/Handler/Search/ESPopulateHandler.php b/databox/api/src/Consumer/Handler/Search/ESPopulateHandler.php index 7658b09db..10d6a8629 100644 --- a/databox/api/src/Consumer/Handler/Search/ESPopulateHandler.php +++ b/databox/api/src/Consumer/Handler/Search/ESPopulateHandler.php @@ -15,8 +15,6 @@ #[AsMessageHandler] final readonly class ESPopulateHandler { - final public const EVENT = 'es_populate'; - public function __construct( private KernelInterface $kernel, private EntityManagerInterface $em, diff --git a/databox/api/src/Controller/Admin/AttributeCrudController.php b/databox/api/src/Controller/Admin/AttributeCrudController.php index 904517d70..674c43eaf 100644 --- a/databox/api/src/Controller/Admin/AttributeCrudController.php +++ b/databox/api/src/Controller/Admin/AttributeCrudController.php @@ -4,8 +4,9 @@ use Alchemy\AdminBundle\Controller\AbstractAdminCrudController; use Alchemy\AdminBundle\Field\IdField; -use App\Entity\Core\Attribute; +use Alchemy\AdminBundle\Field\JsonField; use Alchemy\AdminBundle\Filter\ChildPropertyEntityFilter; +use App\Entity\Core\Attribute; use EasyCorp\Bundle\EasyAdminBundle\Config\Action; use EasyCorp\Bundle\EasyAdminBundle\Config\Actions; use EasyCorp\Bundle\EasyAdminBundle\Config\Crud; @@ -28,7 +29,9 @@ public static function getEntityFqcn(): string public function configureActions(Actions $actions): Actions { return parent::configureActions($actions) - ->remove(Crud::PAGE_INDEX, Action::NEW); + ->add(Crud::PAGE_INDEX, Action::DETAIL) + ->remove(Crud::PAGE_INDEX, Action::NEW) + ; } public function configureFilters(Filters $filters): Filters @@ -50,37 +53,28 @@ public function configureCrud(Crud $crud): Crud public function configureFields(string $pageName): iterable { - $definition = AssociationField::new('definition'); - $value = TextField::new('value'); - $locale = TextField::new('locale'); - $locked = Field::new('locked'); - $origin = IntegerField::new('origin'); - $originVendor = TextField::new('originVendor'); - $originVendorContext = TextareaField::new('originVendorContext'); - $id = IdField::new(); - $position = IntegerField::new('position'); - $translationId = Field::new('translationId'); - $translationOriginHash = TextField::new('translationOriginHash'); - $originUserId = Field::new('originUserId'); - $coordinates = TextareaField::new('coordinates'); - $status = IntegerField::new('status'); - $confidence = NumberField::new('confidence'); - $createdAt = DateTimeField::new('createdAt'); - $updatedAt = DateTimeField::new('updatedAt'); - $asset = AssociationField::new('asset'); - $translationOrigin = AssociationField::new('translationOrigin'); - $translations = AssociationField::new('translations'); - - if (Crud::PAGE_INDEX === $pageName) { - return [$id, $asset, $definition, $value, $locale, $createdAt]; - } elseif (Crud::PAGE_DETAIL === $pageName) { - return [$id, $locale, $locked, $position, $translationId, $translationOriginHash, $value, $origin, $originVendor, $originUserId, $originVendorContext, $coordinates, $status, $confidence, $createdAt, $updatedAt, $asset, $definition, $translationOrigin, $translations]; - } elseif (Crud::PAGE_NEW === $pageName) { - return [$value, $locale, $locked, $origin, $originVendor, $originVendorContext]; - } elseif (Crud::PAGE_EDIT === $pageName) { - return [$definition, $value, $locale, $locked, $origin, $originVendor, $originVendorContext]; - } - - return []; + yield IdField::new(); + yield AssociationField::new('definition'); + yield AssociationField::new('asset'); + yield TextField::new('locale'); + yield TextField::new('value'); + yield Field::new('locked'); + yield IntegerField::new('origin'); + yield TextField::new('originVendor') + ->hideOnIndex(); + yield TextareaField::new('originVendorContext'); + yield IntegerField::new('position'); + yield IdField::new('originUserId') + ->hideOnIndex(); + yield JsonField::new('assetAnnotations') + ->hideOnIndex(); + yield IntegerField::new('status') + ->hideOnIndex(); + yield NumberField::new('confidence') + ->hideOnIndex(); + yield DateTimeField::new('createdAt') + ->hideOnForm(); + yield DateTimeField::new('updatedAt') + ->hideOnForm(); } } diff --git a/databox/api/src/Controller/Admin/AttributeDefinitionCrudController.php b/databox/api/src/Controller/Admin/AttributeDefinitionCrudController.php index 6687d6d94..2a52027c3 100644 --- a/databox/api/src/Controller/Admin/AttributeDefinitionCrudController.php +++ b/databox/api/src/Controller/Admin/AttributeDefinitionCrudController.php @@ -73,6 +73,7 @@ public function configureFields(string $pageName): iterable yield TextField::new('fileType'); yield ChoiceField::new('fieldType') ->setChoices($fileTypeChoices); + yield TextField::new('entityType'); yield BooleanField::new('allowInvalid') ->hideOnIndex() ->renderAsSwitch(false); diff --git a/databox/api/src/Controller/Admin/AttributeEntityCrudController.php b/databox/api/src/Controller/Admin/AttributeEntityCrudController.php new file mode 100644 index 000000000..37c70bdb3 --- /dev/null +++ b/databox/api/src/Controller/Admin/AttributeEntityCrudController.php @@ -0,0 +1,59 @@ +add(Crud::PAGE_INDEX, Action::DETAIL) + ; + } + + public function configureFilters(Filters $filters): Filters + { + return $filters + ->add('type') + ; + } + + public function configureCrud(Crud $crud): Crud + { + return parent::configureCrud($crud) + ->setEntityLabelInSingular('Attribute Entity') + ->setEntityLabelInPlural('Attribute Entities') + ->setSearchFields(['id', 'position', 'type', 'value']) + ->setPaginatorPageSize(20); + } + + public function configureFields(string $pageName): iterable + { + yield IdField::new(); + yield AssociationField::new('workspace'); + yield TextField::new('type'); + yield TextField::new('value'); + yield JsonField::new('translations'); + yield DateTimeField::new('createdAt') + ->hideOnForm(); + yield DateTimeField::new('updatedAt') + ->hideOnForm(); + } +} diff --git a/databox/api/src/Controller/Admin/DashboardController.php b/databox/api/src/Controller/Admin/DashboardController.php index 47687f02f..396f9b5b6 100644 --- a/databox/api/src/Controller/Admin/DashboardController.php +++ b/databox/api/src/Controller/Admin/DashboardController.php @@ -18,6 +18,7 @@ use App\Entity\Core\Attribute; use App\Entity\Core\AttributeClass; use App\Entity\Core\AttributeDefinition; +use App\Entity\Core\AttributeEntity; use App\Entity\Core\Collection; use App\Entity\Core\File; use App\Entity\Core\RenditionClass; @@ -65,6 +66,7 @@ public function configureMenuItems(): iterable MenuItem::linkToCrud('Asset', '', Asset::class), MenuItem::linkToCrud('File', '', File::class), MenuItem::linkToCrud('Attribute', '', Attribute::class), + MenuItem::linkToCrud('Attribute Entity', '', AttributeEntity::class), MenuItem::linkToCrud('AssetTitleAttribute', '', AssetTitleAttribute::class), MenuItem::linkToCrud('AttributeDefinition', '', AttributeDefinition::class), MenuItem::linkToCrud('AttributeClass', '', AttributeClass::class), diff --git a/databox/api/src/Controller/Core/AttributeBatchUpdateAction.php b/databox/api/src/Controller/Core/AttributeBatchUpdateAction.php deleted file mode 100644 index 663ad3f69..000000000 --- a/databox/api/src/Controller/Core/AttributeBatchUpdateAction.php +++ /dev/null @@ -1,38 +0,0 @@ -batchAttributeManager->validate($data->assets, $data); - - if (null !== $workspaceId) { - $this->batchAttributeManager->handleBatch( - $workspaceId, - $data->assets, - $data, - $this->getStrictUser(), - true, - ); - } - - return new Response(''); - } -} diff --git a/databox/api/src/Controller/Integration/ExposeIntegrationController.php b/databox/api/src/Controller/Integration/ExposeIntegrationController.php index c76fc8fcf..5f107c76a 100644 --- a/databox/api/src/Controller/Integration/ExposeIntegrationController.php +++ b/databox/api/src/Controller/Integration/ExposeIntegrationController.php @@ -41,7 +41,6 @@ public function profilesProxy( ); } - #[Route(path: '/{integrationId}/proxy/publications', name: 'proxy_publications')] public function publicationsProxy( string $integrationId, diff --git a/databox/api/src/Doctrine/Listener/AttributeEntityListener.php b/databox/api/src/Doctrine/Listener/AttributeEntityListener.php new file mode 100644 index 000000000..565629ea4 --- /dev/null +++ b/databox/api/src/Doctrine/Listener/AttributeEntityListener.php @@ -0,0 +1,70 @@ +getObjectManager(); + $uow = $em->getUnitOfWork(); + foreach ($uow->getScheduledEntityUpdates() as $entityUpdate) { + if ($entityUpdate instanceof AttributeEntity) { + $changeSet = $uow->getEntityChangeSet($entityUpdate); + $changes = []; + if ($changeSet['value'] ?? false) { + $changes[AttributeInterface::NO_LOCALE] = $changeSet['value'][1]; + } + if ($changeSet['translations'] ?? false) { + [$old, $new] = $changeSet['translations']; + foreach ($new as $l => $v) { + if (isset($old[$l]) && $old[$l] !== $v) { + $changes[$l] = $v; + } + } + } + if (!empty($changes)) { + $this->postFlushStack->addBusMessage(new AttributeEntityUpdate( + $entityUpdate->getId(), + $changes + )); + } + } + } + + foreach ($uow->getScheduledEntityDeletions() as $entity) { + if ($entity instanceof AttributeEntity) { + $this->postFlushStack->addBusMessage(new AttributeEntityDelete( + $entity->getId(), + $entity->getType(), + $entity->getWorkspaceId(), + )); + } + } + } + + public function getSubscribedEvents(): array + { + return [ + Events::onFlush, + ]; + } +} diff --git a/databox/api/src/Doctrine/Listener/CacheInvalidatorListener.php b/databox/api/src/Doctrine/Listener/CacheInvalidatorListener.php deleted file mode 100644 index a962aebfc..000000000 --- a/databox/api/src/Doctrine/Listener/CacheInvalidatorListener.php +++ /dev/null @@ -1,68 +0,0 @@ -getObjectManager(); - - $entity = $args->getObject(); - - if ($entity instanceof AbstractUuidEntity) { - $repo = $em->getRepository($entity::class); - if ($repo instanceof CacheRepositoryInterface) { - $id = $entity->getId(); - $this->postFlushStack->addCallback(function () use ($repo, $id): void { - $repo->invalidateEntity($id); - $repo->invalidateList(); - }); - } - } - } - - public function preRemove(PreRemoveEventArgs $args): void - { - $this->invalidateEntity($args); - } - - public function prePersist(PrePersistEventArgs $args): void - { - $this->invalidateEntity($args); - } - - public function preUpdate(PreUpdateEventArgs $args): void - { - $this->invalidateEntity($args); - } - - public function getSubscribedEvents(): array - { - return [ - Events::preRemove, - Events::prePersist, - Events::preUpdate, - ]; - } -} diff --git a/databox/api/src/Doctrine/Listener/MemoryCacheInvalidatorListener.php b/databox/api/src/Doctrine/Listener/MemoryCacheInvalidatorListener.php deleted file mode 100644 index 6a83e7445..000000000 --- a/databox/api/src/Doctrine/Listener/MemoryCacheInvalidatorListener.php +++ /dev/null @@ -1,31 +0,0 @@ -cache->invalidateList(); - } - - public function getSubscribedEvents(): array - { - return [ - Events::onClear, - ]; - } -} diff --git a/databox/api/src/Elasticsearch/AssetSearch.php b/databox/api/src/Elasticsearch/AssetSearch.php index addeafad9..dc50460da 100644 --- a/databox/api/src/Elasticsearch/AssetSearch.php +++ b/databox/api/src/Elasticsearch/AssetSearch.php @@ -4,6 +4,7 @@ namespace App\Elasticsearch; +use App\Attribute\AttributeInterface; use App\Entity\Core\Asset; use App\Entity\Core\Collection; use App\Entity\Core\Workspace; @@ -33,6 +34,8 @@ public function search( array $groupIds, array $options = [] ): array { + $maxLimit = 50; + $filterQueries = []; $aclBoolQuery = $this->createACLBoolQuery($userId, $groupIds); @@ -50,6 +53,11 @@ public function search( $filterQueries[] = new Query\Terms('collectionPaths', $paths); } + if (isset($options['ids'])) { + $filterQueries[] = new Query\Terms('_id', $options['ids']); + $maxLimit = 500; + } + if (isset($options['workspaces'])) { $filterQueries[] = new Query\Terms('workspaceId', $options['workspaces']); } @@ -82,7 +90,6 @@ public function search( } } - $maxLimit = 50; $limit = $options['limit'] ?? $maxLimit; if ($limit > $maxLimit) { $limit = $maxLimit; @@ -127,7 +134,7 @@ public function search( 'fragment_size' => 255, 'number_of_fragments' => 1, ], - 'attributes.*' => [ + AttributeInterface::ATTRIBUTES_FIELD.'.*' => [ 'number_of_fragments' => 20, ], ], diff --git a/databox/api/src/Elasticsearch/AttributeEntitySearch.php b/databox/api/src/Elasticsearch/AttributeEntitySearch.php new file mode 100644 index 000000000..4a62e69ac --- /dev/null +++ b/databox/api/src/Elasticsearch/AttributeEntitySearch.php @@ -0,0 +1,75 @@ +addFilter(new Query\Term(['workspaceId' => $workspaceId])); + + $queryString = trim($options['query'] ?? ''); + if (!empty($queryString)) { + $match = new Query\MultiMatch(); + $match->setQuery($queryString); + $match->setType('bool_prefix'); + $match->setFields([ + 'value.suggest', + 'value.suggest._2gram', + 'value.suggest._3gram', + ]); + $filterQuery->addMust($match); + } + + $type = trim($options['type'] ?? ''); + if (!empty($type)) { + $filterQuery->addFilter(new Query\Term(['type' => $type])); + } + + $limit = $options['limit'] ?? $maxLimit; + if ($limit > $maxLimit) { + $limit = $maxLimit; + } + + $query = new Query(); + $query->setTrackTotalHits(); + $query->setQuery($filterQuery); + $query->setSort([ + '_score' => 'DESC', + 'value.raw' => 'ASC', + ]); + $query->setHighlight([ + 'pre_tags' => ['[hl]'], + 'post_tags' => ['[/hl]'], + 'fields' => [ + 'value' => [ + 'fragment_size' => 255, + 'number_of_fragments' => 1, + ], + ], + ]); + + $data = $this->finder->findPaginated($query); + $data->setMaxPerPage((int) $limit); + $data->setCurrentPage((int) ($options['page'] ?? 1)); + + return $data; + } +} diff --git a/databox/api/src/Elasticsearch/AttributeSearch.php b/databox/api/src/Elasticsearch/AttributeSearch.php index 88bae56ae..ac8933c21 100644 --- a/databox/api/src/Elasticsearch/AttributeSearch.php +++ b/databox/api/src/Elasticsearch/AttributeSearch.php @@ -4,16 +4,13 @@ namespace App\Elasticsearch; +use App\Attribute\AttributeInterface; use App\Attribute\AttributeTypeRegistry; use App\Attribute\Type\AttributeTypeInterface; -use App\Attribute\Type\DateTimeAttributeType; -use App\Attribute\Type\KeywordAttributeType; -use App\Attribute\Type\NumberAttributeType; use App\Attribute\Type\TextAttributeType; use App\Elasticsearch\Mapping\FieldNameResolver; -use App\Elasticsearch\Mapping\IndexMappingUpdater; use App\Entity\Core\AttributeDefinition; -use App\Repository\Core\AttributeDefinitionRepositoryInterface; +use App\Repository\Core\AttributeDefinitionRepository; use Doctrine\ORM\EntityManagerInterface; use Elastica\Aggregation; use Elastica\Aggregation\Missing; @@ -65,7 +62,7 @@ public function createClustersFromDefinitions(iterable $definitions): array ]; $boost = $d['searchBoost'] ?? 1; - $trIndex = $type->isLocaleAware() && $d['translatable'] ? 1 : 0; + $trIndex = $type->isLocaleAware() && ($d['translatable'] || $type->supportsTranslations()) ? 1 : 0; if ($d['allowed']) { $groups[$fieldName]['w'][$boost] ??= [ @@ -93,7 +90,7 @@ public function createClustersFromDefinitions(iterable $definitions): array ]; $trKey = array_keys($group['w'][$firstBoost])[0]; $st = array_keys($group['w'][$firstBoost][$trKey])[0]; - $fieldName = sprintf('attributes.%s.%s', $trKey ? '{l}' : '_', $f); + $fieldName = sprintf('%s.%s.%s', AttributeInterface::ATTRIBUTES_FIELD, $trKey ? '{l}' : '_', $f); $clusters[self::GROUP_ALL]['fields'][$fieldName] = [ 'st' => $st, @@ -112,7 +109,7 @@ public function createClustersFromDefinitions(iterable $definitions): array 'b' => $boost, 'fields' => [], ]; - $fieldName = sprintf('attributes.%s.%s', $tr ? '{l}' : '_', $f); + $fieldName = sprintf('%s.%s.%s', AttributeInterface::ATTRIBUTES_FIELD, $tr ? '{l}' : '_', $f); $clusters[$uk]['fields'][$fieldName] = [ 'st' => $st, 'b' => $boost, @@ -322,16 +319,16 @@ public function buildFacets( /** @var AttributeDefinition[] $attributeDefinitions */ $attributeDefinitions = $this->em->getRepository(AttributeDefinition::class) ->getSearchableAttributes($userId, $groupIds, [ - AttributeDefinitionRepositoryInterface::OPT_FACET_ENABLED => true, - AttributeDefinitionRepositoryInterface::OPT_TYPES => $facetTypes, + AttributeDefinitionRepository::OPT_FACET_ENABLED => true, + AttributeDefinitionRepository::OPT_TYPES => $facetTypes, ]); $facets = []; foreach ($attributeDefinitions as $definition) { $fieldName = $this->fieldNameResolver->getFieldNameFromDefinition($definition); $type = $this->typeRegistry->getStrictType($definition->getFieldType()); - $l = $type->isLocaleAware() && $definition->isTranslatable() ? $language : IndexMappingUpdater::NO_LOCALE; - $field = sprintf('attributes.%s.%s', $l, $fieldName); + $l = $type->isLocaleAware() && $definition->isTranslatable() ? $language : AttributeInterface::NO_LOCALE; + $field = sprintf('%s.%s.%s', AttributeInterface::ATTRIBUTES_FIELD, $l, $fieldName); if (isset($facets[$field])) { continue; diff --git a/databox/api/src/Elasticsearch/ElasticSearchClient.php b/databox/api/src/Elasticsearch/ElasticSearchClient.php new file mode 100644 index 000000000..e58c76df7 --- /dev/null +++ b/databox/api/src/Elasticsearch/ElasticSearchClient.php @@ -0,0 +1,39 @@ +getIndexName($indexName); + + $this->request($index.'/_refresh'); + + $this->request($index.'/_update_by_query?conflicts=proceed', [ + 'script' => $script, + 'query' => $query, + ]); + } + + public function getIndexName(string $key): string + { + return $this->{$key.'Index'}->getName(); + } + + public function request(string $path, array $data = [], string $method = Request::POST): Response + { + return $this->client->request($path, $method, $data); + } +} diff --git a/databox/api/src/Elasticsearch/Listener/AssetPostTransformListener.php b/databox/api/src/Elasticsearch/Listener/AssetPostTransformListener.php index 827b8128c..47f5d3dde 100644 --- a/databox/api/src/Elasticsearch/Listener/AssetPostTransformListener.php +++ b/databox/api/src/Elasticsearch/Listener/AssetPostTransformListener.php @@ -5,11 +5,13 @@ namespace App\Elasticsearch\Listener; use App\Asset\Attribute\AttributesResolver; +use App\Attribute\AttributeInterface; use App\Attribute\AttributeTypeRegistry; use App\Elasticsearch\AssetPermissionComputer; use App\Elasticsearch\Mapping\FieldNameResolver; use App\Entity\Core\Asset; use App\Entity\Core\AssetRendition; +use App\Entity\Core\Attribute; use App\Entity\Core\RenditionDefinition; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Query\Expr\Join; @@ -41,7 +43,7 @@ public function hydrateDocument(PostTransformEvent $event): void $document->set($key, $value); } - $document->set('attributes', $this->compileAttributes($asset)); + $document->set(AttributeInterface::ATTRIBUTES_FIELD, $this->compileAttributes($asset)); $document->set('renditions', $this->compileRenditions($asset)); } @@ -68,22 +70,22 @@ private function compileAttributes(Asset $asset): array { $data = []; - $attributes = $this->attributesResolver->resolveAssetAttributes($asset, false); + $attributeIndex = $this->attributesResolver->resolveAssetAttributes($asset, false); - foreach ($attributes as $_attrs) { - foreach ($_attrs as $l => $a) { - $definition = $a->getDefinition(); + foreach ($attributeIndex->getDefinitions() as $definitionIndex) { + $definition = $definitionIndex->getDefinition(); + $isMultiple = $definition->isMultiple(); + $type = $this->attributeTypeRegistry->getStrictType($definition->getFieldType()); + $fieldName = null; - $type = $this->attributeTypeRegistry->getStrictType($definition->getFieldType()); - - if ($definition->isMultiple()) { - $v = $a->getValues(); - if (!empty($v)) { - $v = array_map(fn (string $v): string => $type->normalizeElasticsearchValue($v), $v); + foreach ($definitionIndex->getLocales() as $l => $a) { + $v = null; + if ($isMultiple) { + if (!empty($a)) { + $v = array_map(fn (Attribute $v): string|array => $type->normalizeElasticsearchValue($v->getValue()), $a); } } else { $v = $a->getValue(); - if (null !== $v) { $v = $type->normalizeElasticsearchValue($v); } @@ -93,8 +95,25 @@ private function compileAttributes(Asset $asset): array null !== $v && (!is_array($v) || !empty($v)) ) { - $fieldName = $this->fieldNameResolver->getFieldNameFromDefinition($definition); - $data[$l][$fieldName] = $v; + $fieldName = $fieldName ?? $this->fieldNameResolver->getFieldNameFromDefinition($definition); + + if ($type->supportsTranslations()) { + if ($isMultiple) { + foreach ($v as $item) { + foreach ($item as $locale => $translation) { + $data[$locale][$fieldName] ??= []; + $data[$locale][$fieldName][] = $translation; + } + } + + } else { + foreach ($v as $locale => $translation) { + $data[$locale][$fieldName] = $translation; + } + } + } else { + $data[$l][$fieldName] = $v; + } } } } diff --git a/databox/api/src/Elasticsearch/Mapping/FieldNameResolver.php b/databox/api/src/Elasticsearch/Mapping/FieldNameResolver.php index 61b950d77..e6651d882 100644 --- a/databox/api/src/Elasticsearch/Mapping/FieldNameResolver.php +++ b/databox/api/src/Elasticsearch/Mapping/FieldNameResolver.php @@ -4,6 +4,7 @@ namespace App\Elasticsearch\Mapping; +use App\Attribute\AttributeInterface; use App\Attribute\AttributeTypeRegistry; use App\Attribute\Type\AttributeTypeInterface; use App\Elasticsearch\Facet\FacetRegistry; @@ -45,7 +46,7 @@ public function getFieldFromName(string $name): array } else { $info = $this->extractField($name); $type = $info['type']; - $f = sprintf('attributes._.%s', $info['field']); + $f = sprintf('%s._.%s', AttributeInterface::ATTRIBUTES_FIELD, $info['field']); if (null !== $subField = $type->getAggregationField()) { $f .= '.'.$subField; } diff --git a/databox/api/src/Elasticsearch/Mapping/IndexMappingUpdater.php b/databox/api/src/Elasticsearch/Mapping/IndexMappingUpdater.php index e5f11f341..d352f5664 100644 --- a/databox/api/src/Elasticsearch/Mapping/IndexMappingUpdater.php +++ b/databox/api/src/Elasticsearch/Mapping/IndexMappingUpdater.php @@ -4,6 +4,7 @@ namespace App\Elasticsearch\Mapping; +use App\Attribute\AttributeInterface; use App\Attribute\AttributeTypeRegistry; use App\Entity\Core\AttributeDefinition; use App\Entity\Core\Workspace; @@ -12,8 +13,6 @@ final readonly class IndexMappingUpdater { - final public const NO_LOCALE = '_'; - public function __construct( private ElasticsearchClient $client, private Index $index, @@ -26,12 +25,12 @@ public function __construct( public function assignAttributeToMapping(array &$mapping, string $locale, AttributeDefinition $definition): void { $fieldName = $this->fieldNameResolver->getFieldNameFromDefinition($definition); - $mapping['properties']['attributes'] ??= [ + $mapping['properties'][AttributeInterface::ATTRIBUTES_FIELD] ??= [ 'type' => 'object', 'properties' => [], ]; - $properties = &$mapping['properties']['attributes']['properties']; + $properties = &$mapping['properties'][AttributeInterface::ATTRIBUTES_FIELD]['properties']; $properties[$locale] ??= [ 'type' => 'object', @@ -47,24 +46,33 @@ private function getFieldMapping(AttributeDefinition $definition, string $locale { $type = $this->attributeTypeRegistry->getStrictType($definition->getFieldType()); - return array_merge([ + $mapping = array_merge([ 'type' => $type->getElasticSearchType(), 'meta' => [ 'attribute_id' => $definition->getId(), 'attribute_name' => $definition->getName(), ], ], $type->getElasticSearchMapping($locale, $definition)); + + if (in_array($mapping['type'], [ + 'object', + 'nested', + ], true)) { + unset($mapping['meta']); + } + + return $mapping; } public function synchronizeWorkspace(Workspace $workspace): void { $mapping = $this->index->getMapping(); - $attributes = $mapping['properties']['attributes']['properties'] ?? []; + $attributes = $mapping['properties'][AttributeInterface::ATTRIBUTES_FIELD]['properties'] ?? []; $newMapping = [ 'properties' => [ - 'attributes' => [ + AttributeInterface::ATTRIBUTES_FIELD => [ 'type' => 'object', 'properties' => [], ], @@ -114,12 +122,11 @@ public function assignAttributeDefinitionToMapping(array &$newMapping, Attribute } }; - if ($type->isLocaleAware() && $definition->isTranslatable()) { + $assign(AttributeInterface::NO_LOCALE); + if ($type->isLocaleAware() && ($definition->isTranslatable() || $type->supportsTranslations())) { foreach ($workspace->getEnabledLocales() as $locale) { $assign($locale); } - } else { - $assign(self::NO_LOCALE); } return $upsert; diff --git a/databox/api/src/Elasticsearch/SuggestionSearch.php b/databox/api/src/Elasticsearch/SuggestionSearch.php index 2ebfaa9b1..7f765b09b 100644 --- a/databox/api/src/Elasticsearch/SuggestionSearch.php +++ b/databox/api/src/Elasticsearch/SuggestionSearch.php @@ -5,7 +5,7 @@ namespace App\Elasticsearch; use App\Entity\Core\AttributeDefinition; -use App\Repository\Core\AttributeDefinitionRepositoryInterface; +use App\Repository\Core\AttributeDefinitionRepository; use Elastica\Collapse; use Elastica\Query; use Elastica\Result; @@ -54,7 +54,7 @@ public function search( /** @var AttributeDefinition[] $suggestAttributes */ $suggestAttributes = $this->em->getRepository(AttributeDefinition::class) ->getSearchableAttributes($userId, $groupIds, [ - AttributeDefinitionRepositoryInterface::OPT_SUGGEST_ENABLED => true, + AttributeDefinitionRepository::OPT_SUGGEST_ENABLED => true, ]); $definitionNames = []; diff --git a/databox/api/src/Elasticsearch/TagSearch.php b/databox/api/src/Elasticsearch/TagSearch.php new file mode 100644 index 000000000..4259941c2 --- /dev/null +++ b/databox/api/src/Elasticsearch/TagSearch.php @@ -0,0 +1,70 @@ +addFilter(new Query\Term(['workspaceId' => $workspaceId])); + + $queryString = trim($options['query'] ?? ''); + if (!empty($queryString)) { + $match = new Query\MultiMatch(); + $match->setQuery($queryString); + $match->setType('bool_prefix'); + $match->setFields([ + 'name.suggest', + 'name.suggest._2gram', + 'name.suggest._3gram', + ]); + $filterQuery->addMust($match); + } + + $limit = $options['limit'] ?? $maxLimit; + if ($limit > $maxLimit) { + $limit = $maxLimit; + } + + $query = new Query(); + $query->setTrackTotalHits(); + $query->setQuery($filterQuery); + $query->setSort([ + '_score' => 'DESC', + 'name.raw' => 'ASC', + ]); + $query->setHighlight([ + 'pre_tags' => ['[hl]'], + 'post_tags' => ['[/hl]'], + 'fields' => [ + 'name' => [ + 'fragment_size' => 255, + 'number_of_fragments' => 1, + ], + ], + ]); + + $data = $this->finder->findPaginated($query); + $data->setMaxPerPage((int) $limit); + $data->setCurrentPage((int) ($options['page'] ?? 1)); + + return $data; + } +} diff --git a/databox/api/src/Entity/Basket/BasketAsset.php b/databox/api/src/Entity/Basket/BasketAsset.php index 829e78eb7..925d607f9 100644 --- a/databox/api/src/Entity/Basket/BasketAsset.php +++ b/databox/api/src/Entity/Basket/BasketAsset.php @@ -10,6 +10,7 @@ use App\Api\Provider\BasketAssetCollectionProvider; use App\Entity\AbstractUuidEntity; use App\Entity\Core\Asset; +use App\Entity\Traits\AssetAnnotationsTrait; use App\Entity\Traits\CreatedAtTrait; use App\Entity\Traits\OwnerIdTrait; use App\Entity\WithOwnerIdInterface; @@ -39,6 +40,7 @@ class BasketAsset extends AbstractUuidEntity implements WithOwnerIdInterface { use OwnerIdTrait; use CreatedAtTrait; + use AssetAnnotationsTrait; public const GROUP_LIST = 'basket-asset:list'; @@ -79,16 +81,6 @@ public function setAsset(Asset $asset): void $this->asset = $asset; } - public function getClip(): ?array - { - return $this->context['clip'] ?? null; - } - - public function setClip(?array $clip): void - { - $this->context['clip'] = $clip; - } - public function getContext(): ?array { return $this->context; diff --git a/databox/api/src/Entity/Core/AbstractBaseAttribute.php b/databox/api/src/Entity/Core/AbstractBaseAttribute.php index cc5f2286a..af2d7c347 100644 --- a/databox/api/src/Entity/Core/AbstractBaseAttribute.php +++ b/databox/api/src/Entity/Core/AbstractBaseAttribute.php @@ -25,11 +25,6 @@ abstract class AbstractBaseAttribute extends AbstractUuidEntity #[ORM\Column(type: Types::TEXT, nullable: false)] private ?string $value = null; - /** - * Resolved by PHP. - */ - private ?array $values = null; - public function getValue(): ?string { return $this->value; @@ -74,14 +69,4 @@ public function setUpdatedAt(\DateTimeImmutable $updatedAt): void { $this->updatedAt = $updatedAt; } - - public function getValues(): ?array - { - return $this->values; - } - - public function setValues(?array $values): void - { - $this->values = $values; - } } diff --git a/databox/api/src/Entity/Core/AssetRendition.php b/databox/api/src/Entity/Core/AssetRendition.php index f48c17e3b..6db85cb6a 100644 --- a/databox/api/src/Entity/Core/AssetRendition.php +++ b/databox/api/src/Entity/Core/AssetRendition.php @@ -18,6 +18,7 @@ use App\Entity\Traits\CreatedAtTrait; use App\Entity\Traits\UpdatedAtTrait; use App\Repository\Core\AssetRenditionRepository; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; @@ -138,6 +139,13 @@ class AssetRendition extends AbstractUuidEntity #[ORM\JoinColumn(nullable: true)] private ?File $file = null; + /** + * Homothetic and same format has original. + */ + #[Groups([AssetRendition::GROUP_LIST, AssetRendition::GROUP_READ])] + #[ORM\Column(type: Types::BOOLEAN, nullable: true)] + private ?bool $projection = null; + public function getAsset(): Asset { return $this->asset; @@ -180,4 +188,14 @@ public function isReady(): bool { return null !== $this->file; } + + public function getProjection(): ?bool + { + return $this->projection; + } + + public function setProjection(?bool $projection): void + { + $this->projection = $projection; + } } diff --git a/databox/api/src/Entity/Core/Attribute.php b/databox/api/src/Entity/Core/Attribute.php index e949e2c39..0d9c73716 100644 --- a/databox/api/src/Entity/Core/Attribute.php +++ b/databox/api/src/Entity/Core/Attribute.php @@ -19,9 +19,8 @@ use App\Api\Model\Output\AttributeOutput; use App\Api\Processor\BatchAttributeUpdateProcessor; use App\Api\Provider\AttributeCollectionProvider; -use App\Controller\Core\AttributeBatchUpdateAction; +use App\Entity\Traits\AssetAnnotationsTrait; use App\Repository\Core\AttributeRepository; -use Doctrine\Common\Collections\Collection as DoctrineCollection; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Ramsey\Uuid\Doctrine\UuidType; @@ -39,7 +38,7 @@ ), new Post( uriTemplate: '/attributes/batch-update', - controller: AttributeBatchUpdateAction::class, + status: 200, input: AttributeBatchUpdateInput::class, name: 'post_batch', processor: BatchAttributeUpdateProcessor::class, @@ -59,6 +58,8 @@ #[ApiFilter(filterClass: SearchFilter::class, properties: ['asset' => 'exact'])] class Attribute extends AbstractBaseAttribute implements ESIndexableDeleteDependencyInterface { + use AssetAnnotationsTrait; + final public const GROUP_READ = 'attr:read'; final public const GROUP_LIST = 'attr:index'; @@ -95,39 +96,11 @@ class Attribute extends AbstractBaseAttribute implements ESIndexableDeleteDepend #[ORM\JoinColumn(nullable: false)] protected ?AttributeDefinition $definition = null; - /** - * Unique ID to group translations of the same attribute. - */ - #[ORM\Column(type: UuidType::NAME, nullable: true)] - private ?string $translationId = null; - - /** - * Unique ID to group translations of the same attribute. - */ - #[ORM\ManyToOne(targetEntity: Attribute::class, inversedBy: 'translations')] - #[ORM\JoinColumn(nullable: true)] - private ?self $translationOrigin = null; - - /** - * Hashed value of the original translated string. - */ - #[ORM\Column(type: Types::STRING, length: 32, nullable: true)] - private ?string $translationOriginHash = null; - - #[ORM\OneToMany(targetEntity: Attribute::class, mappedBy: 'translationOrigin', cascade: ['remove'])] - #[ORM\JoinColumn(nullable: true)] - private ?DoctrineCollection $translations = null; - /** * Dynamically resolved. */ private ?string $highlight = null; - /** - * Dynamically resolved. - */ - private ?array $highlights = null; - #[ORM\Column(type: Types::SMALLINT, nullable: false)] private ?int $origin = null; @@ -143,9 +116,6 @@ class Attribute extends AbstractBaseAttribute implements ESIndexableDeleteDepend #[ORM\Column(type: Types::TEXT, nullable: true)] private ?string $originVendorContext = null; - #[ORM\Column(type: Types::TEXT, nullable: true)] - private ?string $coordinates = null; - #[ORM\Column(type: Types::SMALLINT, nullable: true)] private int $status = self::STATUS_VALID; @@ -182,16 +152,6 @@ public function getDefinitionId(): string return $this->definition->getId(); } - public function getTranslationId(): ?string - { - return $this->translationId; - } - - public function setTranslationId(?string $translationId): void - { - $this->translationId = $translationId; - } - public function hasOrigin(): bool { return null !== $this->origin; @@ -242,16 +202,6 @@ public function setOriginVendorContext(?string $originVendorContext): void $this->originVendorContext = $originVendorContext; } - public function getCoordinates(): ?string - { - return $this->coordinates; - } - - public function setCoordinates(?string $coordinates): void - { - $this->coordinates = $coordinates; - } - public function getStatus(): int { return $this->status; @@ -294,31 +244,6 @@ public function setHighlight(?string $highlight): void $this->highlight = $highlight; } - public function getHighlights(): ?array - { - return $this->highlights; - } - - public function setHighlights(?array $highlights): void - { - $this->highlights = $highlights; - } - - public function getTranslationOrigin(): ?Attribute - { - return $this->translationOrigin; - } - - public function getTranslationOriginHash(): ?string - { - return $this->translationOriginHash; - } - - public function setTranslationOriginHash(?string $translationOriginHash): void - { - $this->translationOriginHash = $translationOriginHash; - } - public function isLocked(): bool { return $this->locked; diff --git a/databox/api/src/Entity/Core/AttributeDefinition.php b/databox/api/src/Entity/Core/AttributeDefinition.php index ac7ac187f..237e9d4a0 100644 --- a/databox/api/src/Entity/Core/AttributeDefinition.php +++ b/databox/api/src/Entity/Core/AttributeDefinition.php @@ -16,9 +16,9 @@ use App\Api\Model\Input\AttributeDefinitionInput; use App\Api\Model\Output\AttributeDefinitionOutput; use App\Api\Provider\AttributeDefinitionCollectionProvider; +use App\Attribute\AttributeInterface; use App\Attribute\Type\TextAttributeType; use App\Controller\Core\AttributeDefinitionSortAction; -use App\Elasticsearch\Mapping\IndexMappingUpdater; use App\Entity\AbstractUuidEntity; use App\Entity\Traits\CreatedAtTrait; use App\Entity\Traits\UpdatedAtTrait; @@ -123,10 +123,14 @@ class AttributeDefinition extends AbstractUuidEntity implements \Stringable #[ORM\Column(type: Types::STRING, length: 100, nullable: true)] private ?string $fileType = null; - #[Groups([AttributeDefinition::GROUP_LIST, Asset::GROUP_LIST])] + #[Groups([AttributeDefinition::GROUP_LIST, Asset::GROUP_LIST, Asset::GROUP_READ])] #[ORM\Column(type: Types::STRING, length: 50, nullable: false)] private string $fieldType = TextAttributeType::NAME; + #[Groups([AttributeDefinition::GROUP_LIST, Asset::GROUP_LIST, Asset::GROUP_READ])] + #[ORM\Column(type: Types::STRING, length: AttributeEntity::TYPE_LENGTH, nullable: true)] + private ?string $entityType = null; + #[Groups([AttributeDefinition::GROUP_LIST])] #[ORM\Column(type: Types::BOOLEAN, nullable: false)] private bool $searchable = true; @@ -239,12 +243,12 @@ public function setFallbackFR(?string $fallback): void public function setFallbackAll(?string $fallback): void { - $this->fallback[IndexMappingUpdater::NO_LOCALE] = $fallback; + $this->fallback[AttributeInterface::NO_LOCALE] = $fallback; } public function getFallbackAll(): ?string { - return $this->fallback[IndexMappingUpdater::NO_LOCALE] ?? null; + return $this->fallback[AttributeInterface::NO_LOCALE] ?? null; } public function getFallbackEN(): ?string @@ -384,12 +388,12 @@ public function setInitialValues(?array $initialValues): void public function getInitialValuesAll(): ?string { - return $this->initialValues[IndexMappingUpdater::NO_LOCALE] ?? null; + return $this->initialValues[AttributeInterface::NO_LOCALE] ?? null; } public function setInitialValuesAll(?string $initializer): void { - $this->initialValues[IndexMappingUpdater::NO_LOCALE] = $initializer; + $this->initialValues[AttributeInterface::NO_LOCALE] = $initializer; $this->normalizeInitialValues(); } @@ -429,4 +433,14 @@ public function setSuggest(bool $suggest): void { $this->suggest = $suggest; } + + public function getEntityType(): ?string + { + return $this->entityType; + } + + public function setEntityType(?string $entityType): void + { + $this->entityType = $entityType; + } } diff --git a/databox/api/src/Entity/Core/AttributeEntity.php b/databox/api/src/Entity/Core/AttributeEntity.php new file mode 100644 index 000000000..fa968720a --- /dev/null +++ b/databox/api/src/Entity/Core/AttributeEntity.php @@ -0,0 +1,119 @@ + [ + self::GROUP_LIST, + ], + ], + provider: AttributeEntityCollectionProvider::class, +)] + +#[ORM\Entity(repositoryClass: AttributeEntityRepository::class)] +#[ApiFilter(filterClass: SearchFilter::class, strategy: 'exact', properties: [ + 'workspace', + 'type', +])] +#[ORM\Index(columns: ['type'], name: 'attr_entity_type_idx')] +class AttributeEntity extends AbstractUuidEntity +{ + use CreatedAtTrait; + use UpdatedAtTrait; + use WorkspaceTrait; + public const TYPE_LENGTH = 100; + + final public const GROUP_READ = 'attr-entity:read'; + final public const GROUP_LIST = 'attr-entity:index'; + + #[ORM\Column(type: Types::STRING, length: self::TYPE_LENGTH, nullable: false)] + #[Groups([self::GROUP_LIST, self::GROUP_READ])] + #[NotBlank] + private ?string $type = null; + + #[ORM\Column(type: Types::TEXT, nullable: false)] + #[Groups([self::GROUP_LIST, self::GROUP_READ])] + #[NotBlank] + private ?string $value = null; + + #[ORM\Column(type: Types::INTEGER, nullable: false)] + private int $position = 0; + + #[ORM\Column(type: Types::JSON, nullable: true)] + #[Groups([self::GROUP_LIST, self::GROUP_READ])] + private ?array $translations = null; + + public function getType(): ?string + { + return $this->type; + } + + public function setType(?string $type): void + { + $this->type = $type; + } + + public function getValue(): ?string + { + return $this->value; + } + + public function setValue(?string $value): void + { + $this->value = $value; + } + + public function getPosition(): int + { + return $this->position; + } + + public function setPosition(int $position): void + { + $this->position = $position; + } + + public function getTranslations(): ?array + { + return $this->translations; + } + + public function setTranslations(?array $translations): void + { + $this->translations = $translations; + } +} diff --git a/databox/api/src/Entity/Core/Tag.php b/databox/api/src/Entity/Core/Tag.php index a2b44edea..f34a4e650 100644 --- a/databox/api/src/Entity/Core/Tag.php +++ b/databox/api/src/Entity/Core/Tag.php @@ -14,12 +14,14 @@ use ApiPlatform\Metadata\Put; use App\Api\Model\Input\TagInput; use App\Api\Model\Output\TagOutput; +use App\Api\Provider\TagCollectionProvider; use App\Entity\AbstractUuidEntity; use App\Entity\Traits\CreatedAtTrait; use App\Entity\Traits\LocaleTrait; use App\Entity\Traits\UpdatedAtTrait; use App\Entity\Traits\WorkspaceTrait; use App\Entity\TranslatableInterface; +use App\Repository\Core\TagRepository; use App\Security\Voter\AbstractVoter; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; @@ -59,10 +61,11 @@ ]], input: TagInput::class, output: TagOutput::class, + provider: TagCollectionProvider::class, )] #[ORM\Table] #[ORM\UniqueConstraint(name: 'ws_name_uniq', columns: ['workspace_id', 'name'])] -#[ORM\Entity] +#[ORM\Entity(repositoryClass: TagRepository::class)] #[ApiFilter(filterClass: SearchFilter::class, strategy: 'exact', properties: ['workspace'])] class Tag extends AbstractUuidEntity implements TranslatableInterface, \Stringable { diff --git a/databox/api/src/Entity/Traits/AssetAnnotationsInterface.php b/databox/api/src/Entity/Traits/AssetAnnotationsInterface.php new file mode 100644 index 000000000..92ad58fab --- /dev/null +++ b/databox/api/src/Entity/Traits/AssetAnnotationsInterface.php @@ -0,0 +1,29 @@ + new Assert\Choice(AssetAnnotationsInterface::TYPES), + ], + allowExtraFields: true, + )] + #[Groups([AssetRendition::GROUP_LIST, AssetRendition::GROUP_READ])] + #[ORM\Column(type: Types::JSON, nullable: true)] + private ?array $assetAnnotations = null; + + public function getAssetAnnotations(): ?array + { + return $this->assetAnnotations; + } + + public function setAssetAnnotations(?array $assetAnnotations): void + { + $this->assetAnnotations = $assetAnnotations; + } +} diff --git a/databox/api/src/Fixture/Faker/AssetAnnotationsFaker.php b/databox/api/src/Fixture/Faker/AssetAnnotationsFaker.php new file mode 100644 index 000000000..0b5dd6899 --- /dev/null +++ b/databox/api/src/Fixture/Faker/AssetAnnotationsFaker.php @@ -0,0 +1,84 @@ + AssetAnnotationsInterface::TYPE_CUE, + 't' => rand(0, 10), + ], + ]; + } + + public function assetAnnotationsCircle(): array + { + $x = rand(5, 100) / 100; + $y = rand(5, 100) / 100; + + return [ + [ + 'type' => AssetAnnotationsInterface::TYPE_CIRCLE, + 'x' => $x, + 'y' => $y, + 'r' => min(1 - $x, 1 - $y, rand(5, 50) / 100), + 'c' => $this->randomColor(), + ], + ]; + } + + public function assetAnnotationsPoint(): array + { + $x = rand(0, 100) / 100; + $y = rand(0, 100) / 100; + + return [ + [ + 'type' => AssetAnnotationsInterface::TYPE_POINT, + 'x' => $x, + 'y' => $y, + 'c' => $this->randomColor(), + ], + ]; + } + + public function assetAnnotationsRect(): array + { + $x1 = rand(0, 100) / 100; + $y1 = rand(0, 100) / 100; + $x2 = min($x1 * (1 + rand(10, 100) / 100), 1); + $y2 = min($y1 * (1 + rand(10, 100) / 100), 1); + + return [ + [ + 'type' => AssetAnnotationsInterface::TYPE_RECTANGLE, + 'x1' => $x1, + 'y1' => $y1, + 'x2' => $x2, + 'y2' => $y2, + 'c' => $this->randomColor(), + ], + ]; + } + + private function randomColor(): string + { + $rouge = dechex(rand(0, 255)); + $vert = dechex(rand(0, 255)); + $bleu = dechex(rand(0, 255)); + + $rouge = str_pad($rouge, 2, '0', STR_PAD_LEFT); + $vert = str_pad($vert, 2, '0', STR_PAD_LEFT); + $bleu = str_pad($bleu, 2, '0', STR_PAD_LEFT); + + return '#'.$rouge.$vert.$bleu; + } +} diff --git a/databox/api/src/Integration/Core/Watermark/WatermarkAction.php b/databox/api/src/Integration/Core/Watermark/WatermarkAction.php index da7777acc..4c7f5299f 100644 --- a/databox/api/src/Integration/Core/Watermark/WatermarkAction.php +++ b/databox/api/src/Integration/Core/Watermark/WatermarkAction.php @@ -7,8 +7,8 @@ use Alchemy\Workflow\Executor\RunContext; use App\Asset\Attribute\AttributesResolver; use App\Asset\FileFetcher; +use App\Attribute\AttributeInterface; use App\Attribute\AttributeManager; -use App\Elasticsearch\Mapping\IndexMappingUpdater; use App\Entity\Core\Asset; use App\Entity\Core\AssetRendition; use App\Image\ImageManagerFactory; @@ -38,14 +38,13 @@ public function handle(RunContext $context): void $config = $this->getIntegrationConfig($context); $manager = $this->imageManagerFactory->createManager(); - $attributes = $this->attributesResolver->resolveAssetAttributes($asset, false); + $attributeIndex = $this->attributesResolver->resolveAssetAttributes($asset, false); $attrName = $config['attributeName']; $attrDef = $this->attributeManager->getAttributeDefinitionBySlug($asset->getWorkspaceId(), $attrName) ?? throw new \InvalidArgumentException(sprintf('Attribute definition slug "%s" not found in workspace "%s"', $attrName, $asset->getWorkspaceId())); - $attr = $attributes[$attrDef->getId()][IndexMappingUpdater::NO_LOCALE] ?? null; - $text = $attr?->getValue() ?? null; + $text = $attributeIndex->getAttribute($attrDef->getId(), AttributeInterface::NO_LOCALE)?->getValue(); if (empty($text)) { return; diff --git a/databox/api/src/Integration/Phrasea/Expose/ExposeClient.php b/databox/api/src/Integration/Phrasea/Expose/ExposeClient.php index d6895cec7..3e5787b44 100644 --- a/databox/api/src/Integration/Phrasea/Expose/ExposeClient.php +++ b/databox/api/src/Integration/Phrasea/Expose/ExposeClient.php @@ -5,9 +5,8 @@ use App\Asset\Attribute\AssetTitleResolver; use App\Asset\Attribute\AttributesResolver; use App\Asset\FileFetcher; -use App\Elasticsearch\Mapping\IndexMappingUpdater; +use App\Attribute\AttributeInterface; use App\Entity\Core\Asset; -use App\Entity\Core\AssetRendition; use App\Entity\Core\Attribute; use App\Entity\Integration\IntegrationToken; use App\Integration\IntegrationConfig; @@ -67,8 +66,8 @@ public function getPublication(IntegrationConfig $config, IntegrationToken $inte public function postAsset(IntegrationConfig $config, IntegrationToken $integrationToken, string $publicationId, Asset $asset, array $extraData = []): void { - $attributes = $this->attributesResolver->resolveAssetAttributes($asset, true); - $resolvedTitleAttr = $this->assetTitleResolver->resolveTitle($asset, $attributes, []); + $attributesIndex = $this->attributesResolver->resolveAssetAttributes($asset, true); + $resolvedTitleAttr = $this->assetTitleResolver->resolveTitle($asset, $attributesIndex, []); if ($resolvedTitleAttr instanceof Attribute) { $resolvedTitle = $resolvedTitleAttr->getValue(); } else { @@ -76,34 +75,32 @@ public function postAsset(IntegrationConfig $config, IntegrationToken $integrati } $descriptionTranslations = []; - if (!empty($attributes)) { - foreach ($attributes as $defAttrs) { - $attrTranslations = []; + foreach ($attributesIndex->getDefinitions() as $definitionIndex) { + $attrTranslations = []; - foreach ($defAttrs as $locale => $attribute) { - $attributeDefinition = $attribute->getDefinition(); - $fieldType = $attributeDefinition->getFieldType(); + foreach ($definitionIndex->getLocales() as $locale => $attribute) { + $definition = $definitionIndex->getDefinition(); + $fieldType = $definition->getFieldType(); - $attrTranslations[$locale] = sprintf( - '
%3$s
+ $attrTranslations[$locale] = sprintf( + '
%3$s
%4$s
', - $fieldType, - $attributeDefinition->getSlug(), - $attributeDefinition->getName(), - $attributeDefinition->isMultiple() ? implode(', ', $attribute->getValues()) : $attribute->getValue(), - ); - } + $fieldType, + $definition->getSlug(), + $definition->getName(), + $definition->isMultiple() ? implode(', ', array_map(fn (Attribute $a): ?string => $a->getValue(), $attribute)) : $attribute->getValue(), + ); + } - // adding fallback if not set - if (!isset($attrTranslations[IndexMappingUpdater::NO_LOCALE])) { - $attrTranslations[IndexMappingUpdater::NO_LOCALE] = reset($attrTranslations); - } + // adding fallback if not set + if (!isset($attrTranslations[AttributeInterface::NO_LOCALE])) { + $attrTranslations[AttributeInterface::NO_LOCALE] = reset($attrTranslations); + } - foreach ($attrTranslations as $locale => $translation) { - $descriptionTranslations[$locale] ??= []; - $descriptionTranslations[$locale][] = $translation; - } + foreach ($attrTranslations as $locale => $translation) { + $descriptionTranslations[$locale] ??= []; + $descriptionTranslations[$locale][] = $translation; } } @@ -115,9 +112,9 @@ public function postAsset(IntegrationConfig $config, IntegrationToken $integrati %s', implode("\n", $ltr)); }, $descriptionTranslations); - if (isset($descriptionTranslations[IndexMappingUpdater::NO_LOCALE])) { - $description = $descriptionTranslations[IndexMappingUpdater::NO_LOCALE]; - unset($descriptionTranslations[IndexMappingUpdater::NO_LOCALE]); + if (isset($descriptionTranslations[AttributeInterface::NO_LOCALE])) { + $description = $descriptionTranslations[AttributeInterface::NO_LOCALE]; + unset($descriptionTranslations[AttributeInterface::NO_LOCALE]); } else { $description = array_shift($descriptionTranslations); } @@ -163,7 +160,7 @@ public function postAsset(IntegrationConfig $config, IntegrationToken $integrati foreach ([ 'preview', 'thumbnail', - ] as $renditionName) { + ] as $renditionName) { if (null !== $rendition = $this->renditionManager->getAssetRenditionUsedAs($renditionName, $asset->getId())) { $file = $rendition->getFile(); $subDefFetchedFile = $this->fileFetcher->getFile($file); @@ -171,17 +168,17 @@ public function postAsset(IntegrationConfig $config, IntegrationToken $integrati $subDefResponse = $this->create($config, $integrationToken) ->request('POST', '/sub-definitions', [ 'json' => [ - 'asset_id' => $exposeAssetId, - 'name' => $renditionName, - 'use_as_preview' => 'preview' === $renditionName, - 'use_as_thumbnail' => 'thumbnail' === $renditionName, - 'use_as_poster' => 'poster' === $renditionName, + 'asset_id' => $exposeAssetId, + 'name' => $renditionName, + 'use_as_preview' => 'preview' === $renditionName, + 'use_as_thumbnail' => 'thumbnail' === $renditionName, + 'use_as_poster' => 'poster' === $renditionName, 'upload' => [ 'type' => $file->getType(), 'size' => $file->getSize(), 'name' => $file->getOriginalName(), - ] + ], ], ]) ->toArray() diff --git a/databox/api/src/Integration/Phrasea/Expose/ExposeSynchronizer.php b/databox/api/src/Integration/Phrasea/Expose/ExposeSynchronizer.php index 833ccb1c1..b05c2336b 100644 --- a/databox/api/src/Integration/Phrasea/Expose/ExposeSynchronizer.php +++ b/databox/api/src/Integration/Phrasea/Expose/ExposeSynchronizer.php @@ -27,7 +27,7 @@ public function synchronize(IntegrationData $basketData): void 'sync:'.$basketData->getId(), 30, 'synchronize', - fn() => $this->doSynchronize($basketData) + fn () => $this->doSynchronize($basketData) ); } diff --git a/databox/api/src/Repository/Cache/AttributeDefinitionRepositoryMemoryCachedDecorator.php b/databox/api/src/Repository/Cache/AttributeDefinitionRepositoryMemoryCachedDecorator.php deleted file mode 100644 index 2c6a03111..000000000 --- a/databox/api/src/Repository/Cache/AttributeDefinitionRepositoryMemoryCachedDecorator.php +++ /dev/null @@ -1,86 +0,0 @@ -decorated = $decorated; - - $manager = $registry->getManagerForClass(AttributeDefinition::class); - - parent::__construct($manager, $manager->getClassMetadata(AttributeDefinition::class)); - } - - public function getSearchableAttributes( - ?string $userId, - array $groupIds, - array $options = [] - ): array { - return $this->decorated->getSearchableAttributes($userId, $groupIds, $options); - } - - public function getSearchableAttributesWithPermission( - ?string $userId, - array $groupIds - ): iterable { - return $this->decorated->getSearchableAttributesWithPermission($userId, $groupIds); - } - - public function findByKey(string $key, string $workspaceId): ?AttributeDefinition - { - return $this->decorated->findByKey($key, $workspaceId); - } - - public function getWorkspaceFallbackDefinitions(string $workspaceId): array - { - return $this->cache->get(sprintf('attr_def_fb_%s', $workspaceId), function (ItemInterface $item) use ($workspaceId) { - $item->tag(self::LIST_TAG); - - return $this->decorated->getWorkspaceFallbackDefinitions($workspaceId); - }); - } - - public function getWorkspaceInitializeDefinitions(string $workspaceId): array - { - return $this->cache->get(sprintf('attr_def_ini_%s', $workspaceId), function (ItemInterface $item) use ($workspaceId) { - $item->tag(self::LIST_TAG); - - return $this->decorated->getWorkspaceInitializeDefinitions($workspaceId); - }); - } - - public function getWorkspaceDefinitions(string $workspaceId): array - { - return $this->cache->get(sprintf('attr_defs_%s', $workspaceId), function (ItemInterface $item) use ($workspaceId) { - $item->tag(self::LIST_TAG); - - return $this->decorated->getWorkspaceDefinitions($workspaceId); - }); - } - - public function invalidateEntity(string $id): void - { - } - - public function invalidateList(): void - { - $this->cache->invalidateTags([ - self::LIST_TAG, - ]); - } -} diff --git a/databox/api/src/Repository/Cache/CacheDecoratorTrait.php b/databox/api/src/Repository/Cache/CacheDecoratorTrait.php deleted file mode 100644 index c02b4e31a..000000000 --- a/databox/api/src/Repository/Cache/CacheDecoratorTrait.php +++ /dev/null @@ -1,49 +0,0 @@ -decorated->createQueryBuilder($alias, $indexBy); - } - - public function createResultSetMappingBuilder($alias): ResultSetMappingBuilder - { - return $this->decorated->createResultSetMappingBuilder($alias); - } - - public function find($id, $lockMode = null, $lockVersion = null): ?object - { - return $this->decorated->find($id, $lockMode, $lockVersion); - } - - public function findAll(): array - { - return $this->decorated->findAll(); - } - - public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array - { - return $this->decorated->findBy($criteria, $orderBy, $limit, $offset); - } - - public function findOneBy(array $criteria, ?array $orderBy = null): ?object - { - return $this->decorated->findOneBy($criteria, $orderBy); - } - - public function getClassName() - { - return $this->decorated->getClassName(); - } -} diff --git a/databox/api/src/Repository/Cache/CacheRepositoryInterface.php b/databox/api/src/Repository/Cache/CacheRepositoryInterface.php deleted file mode 100644 index 948e305bc..000000000 --- a/databox/api/src/Repository/Cache/CacheRepositoryInterface.php +++ /dev/null @@ -1,12 +0,0 @@ -getOneOrNullResult(); } + /** + * @return AttributeDefinition[] + */ public function getWorkspaceFallbackDefinitions(string $workspaceId): array { return $this @@ -173,6 +181,9 @@ public function getWorkspaceFallbackDefinitions(string $workspaceId): array ->getResult(); } + /** + * @return AttributeDefinition[] + */ public function getWorkspaceInitializeDefinitions(string $workspaceId): array { return $this @@ -184,6 +195,9 @@ public function getWorkspaceInitializeDefinitions(string $workspaceId): array ->getResult(); } + /** + * @return AttributeDefinition[] + */ public function getWorkspaceDefinitions(string $workspaceId): array { return $this @@ -193,4 +207,21 @@ public function getWorkspaceDefinitions(string $workspaceId): array ->getQuery() ->getResult(); } + + /** + * @return AttributeDefinition[] + */ + public function getWorkspaceDefinitionOfEntity(string $workspaceId, string $entityType): array + { + return $this + ->createQueryBuilder('d') + ->andWhere('d.workspace = :workspace') + ->andWhere('d.fieldType = :t') + ->andWhere('d.entityType = :etype') + ->setParameter('workspace', $workspaceId) + ->setParameter('t', EntityAttributeType::getName()) + ->setParameter('etype', $entityType) + ->getQuery() + ->getResult(); + } } diff --git a/databox/api/src/Repository/Core/AttributeDefinitionRepositoryInterface.php b/databox/api/src/Repository/Core/AttributeDefinitionRepositoryInterface.php deleted file mode 100644 index 7bdd0feec..000000000 --- a/databox/api/src/Repository/Core/AttributeDefinitionRepositoryInterface.php +++ /dev/null @@ -1,40 +0,0 @@ -createQueryBuilder('t') + ->addOrderBy('t.createdAt', 'DESC') + ->addOrderBy('t.id', 'ASC') + ; + } +} diff --git a/databox/api/src/Repository/Core/AttributeRepository.php b/databox/api/src/Repository/Core/AttributeRepository.php index 70836b675..9c6224d7c 100644 --- a/databox/api/src/Repository/Core/AttributeRepository.php +++ b/databox/api/src/Repository/Core/AttributeRepository.php @@ -6,6 +6,7 @@ use App\Attribute\AttributeTypeRegistry; use App\Attribute\Type\AttributeTypeInterface; +use App\Attribute\Type\EntityAttributeType; use App\Entity\Core\Asset; use App\Entity\Core\Attribute; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; @@ -53,13 +54,13 @@ public function getDuplicates(Attribute $attribute): array ->getResult(); } - public function getAssetAttributes(Asset $asset): array + public function getAssetAttributes(string $assetId): array { return $this ->createQueryBuilder('a') ->select('a') ->andWhere('a.asset = :asset') - ->setParameter('asset', $asset->getId()) + ->setParameter('asset', $assetId) ->innerJoin('a.definition', 'd') ->addOrderBy('d.position', 'ASC') ->addOrderBy('d.name', 'ASC') @@ -108,4 +109,30 @@ public function getESQueryBuilder(): QueryBuilder return $queryBuilder; } + + public function deleteByAttributeEntity(string $entityId, string $workspaceId, string $entityType): void + { + $expr = $this->_em->getExpressionBuilder(); + $this + ->createQueryBuilder('t') + ->delete() + ->andWhere($expr->in( + 't.id', + $this + ->createQueryBuilder('a') + ->select('a.id') + ->innerJoin('a.definition', 'd') + ->andWhere('d.workspace = :workspace') + ->andWhere('d.fieldType = :t') + ->andWhere('d.entityType = :etype') + ->andWhere('a.value = :id') + ->getDQL() + )) + ->setParameter('workspace', $workspaceId) + ->setParameter('t', EntityAttributeType::getName()) + ->setParameter('etype', $entityType) + ->setParameter('id', $entityId) + ->getQuery() + ->execute(); + } } diff --git a/databox/api/src/Repository/Core/AttributeRepositoryInterface.php b/databox/api/src/Repository/Core/AttributeRepositoryInterface.php index c3eeed32c..6b0f725ee 100644 --- a/databox/api/src/Repository/Core/AttributeRepositoryInterface.php +++ b/databox/api/src/Repository/Core/AttributeRepositoryInterface.php @@ -4,7 +4,6 @@ namespace App\Repository\Core; -use App\Entity\Core\Asset; use App\Entity\Core\Attribute; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ObjectRepository; @@ -18,7 +17,7 @@ interface AttributeRepositoryInterface extends ObjectRepository */ public function getDuplicates(Attribute $attribute): array; - public function getAssetAttributes(Asset $asset): array; + public function getAssetAttributes(string $assetId): array; public function getESQueryBuilder(): QueryBuilder; } diff --git a/databox/api/src/Repository/Core/TagRepository.php b/databox/api/src/Repository/Core/TagRepository.php new file mode 100644 index 000000000..55193f0e7 --- /dev/null +++ b/databox/api/src/Repository/Core/TagRepository.php @@ -0,0 +1,28 @@ +createQueryBuilder('t') + ->addOrderBy('t.createdAt', 'DESC') + ->addOrderBy('t.id', 'ASC') + ; + } +} diff --git a/databox/api/src/Security/Voter/AttributeDefinitionVoter.php b/databox/api/src/Security/Voter/AttributeDefinitionVoter.php index 1a2ad1d00..3ecd064fa 100644 --- a/databox/api/src/Security/Voter/AttributeDefinitionVoter.php +++ b/databox/api/src/Security/Voter/AttributeDefinitionVoter.php @@ -4,11 +4,14 @@ namespace App\Security\Voter; +use Alchemy\AclBundle\Security\PermissionInterface; use App\Entity\Core\AttributeDefinition; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; class AttributeDefinitionVoter extends AbstractVoter { + final public const VIEW_ATTRIBUTES = 'VIEW_ATTRIBUTES'; + protected function supports(string $attribute, $subject): bool { return $subject instanceof AttributeDefinition; @@ -27,6 +30,8 @@ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $ $workspace = $subject->getWorkspace(); return match ($attribute) { + self::VIEW_ATTRIBUTES => $subject->getClass()->isPublic() + || $this->hasAcl(PermissionInterface::VIEW, $subject->getClass(), $token), self::READ, self::CREATE, self::EDIT, self::DELETE => $this->security->isGranted(self::EDIT, $workspace), default => false, }; diff --git a/databox/api/src/Security/Voter/AttributeEntityVoter.php b/databox/api/src/Security/Voter/AttributeEntityVoter.php new file mode 100644 index 000000000..625682a56 --- /dev/null +++ b/databox/api/src/Security/Voter/AttributeEntityVoter.php @@ -0,0 +1,39 @@ + $this->security->isGranted(AbstractVoter::EDIT, $subject->getWorkspace()); + + return match ($attribute) { + self::CREATE => $workspaceEditor() || $this->security->isGranted(self::SCOPE_PREFIX.'CREATE'), + self::EDIT => $workspaceEditor() || $this->security->isGranted(self::SCOPE_PREFIX.'EDIT'), + self::DELETE => $workspaceEditor() || $this->security->isGranted(self::SCOPE_PREFIX.'DELETE'), + self::READ => true, + default => false, + }; + } +} diff --git a/databox/api/src/Security/Voter/AttributeVoter.php b/databox/api/src/Security/Voter/AttributeVoter.php index 0aa117c40..61236f222 100644 --- a/databox/api/src/Security/Voter/AttributeVoter.php +++ b/databox/api/src/Security/Voter/AttributeVoter.php @@ -25,16 +25,18 @@ public function supportsType(string $subjectType): bool */ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool { + $attributeDefinition = $subject->getDefinition(); + return match ($attribute) { self::READ => $this->security->isGranted(self::READ, $subject->getAsset()) && ( - $subject->getDefinition()->getClass()->isPublic() - || $this->hasAcl(PermissionInterface::VIEW, $subject->getDefinition()->getClass(), $token) + $attributeDefinition->getClass()->isPublic() + || $this->hasAcl(PermissionInterface::VIEW, $attributeDefinition->getClass(), $token) ), self::CREATE, self::EDIT, self::DELETE => $this->security->isGranted(AssetVoter::EDIT_ATTRIBUTES, $subject->getAsset()) && ( - $subject->getDefinition()->getClass()->isEditable() - || $this->hasAcl(PermissionInterface::EDIT, $subject->getDefinition()->getClass(), $token) + $attributeDefinition->getClass()->isEditable() + || $this->hasAcl(PermissionInterface::EDIT, $attributeDefinition->getClass(), $token) ), default => false, }; diff --git a/databox/api/src/Storage/RenditionManager.php b/databox/api/src/Storage/RenditionManager.php index 4c9193280..671f1f44a 100644 --- a/databox/api/src/Storage/RenditionManager.php +++ b/databox/api/src/Storage/RenditionManager.php @@ -120,7 +120,7 @@ public function getAssetRenditionUsedAs(string $as, string $assetId): ?AssetRend ->from(AssetRendition::class, 'r') ->innerJoin('r.definition', 'd') ->andWhere('r.asset = :asset') - ->andWhere(sprintf("d.useAs%s = :as", ucfirst($as))) + ->andWhere(sprintf('d.useAs%s = :as', ucfirst($as))) ->setParameters([ 'asset' => $assetId, 'as' => true, diff --git a/databox/api/tests/Api/AssetAttributeBatchUpdateTest.php b/databox/api/tests/Api/AssetAttributeBatchUpdateTest.php index df6425d48..9041f9944 100644 --- a/databox/api/tests/Api/AssetAttributeBatchUpdateTest.php +++ b/databox/api/tests/Api/AssetAttributeBatchUpdateTest.php @@ -78,12 +78,17 @@ public function testAssetAttributesBatchUpdateOK(array $actions, array $expected ksort($expectedValues); foreach ($expectedValues as $name => $value) { - $attrAssertions[] = [ - 'definition' => [ - 'name' => $name, - ], - 'value' => $value, - ]; + if (!is_array($value)) { + $value = [$value]; + } + foreach ($value as $v) { + $attrAssertions[] = [ + 'definition' => [ + 'name' => $name, + ], + 'value' => $v, + ]; + } } $this->assertJsonContains([ '@type' => 'asset', diff --git a/databox/api/tests/Api/AttributeBatchUpdateTest.php b/databox/api/tests/Api/AttributeBatchUpdateTest.php index 4bcf18aa2..ac7e982e3 100644 --- a/databox/api/tests/Api/AttributeBatchUpdateTest.php +++ b/databox/api/tests/Api/AttributeBatchUpdateTest.php @@ -6,6 +6,7 @@ use Alchemy\AuthBundle\Tests\Client\KeycloakClientTestMock; use App\Entity\Core\Asset; +use App\Entity\Core\Workspace; use App\Tests\AbstractSearchTestCase; use Doctrine\ORM\EntityManagerInterface; use Symfony\Contracts\HttpClient\ResponseInterface; @@ -57,7 +58,7 @@ public function testAttributesBatchUpdateWithInvalidAttributeId(): void public function testAttributesBatchUpdateOK(array $actions, array $expectedAssets): void { $response = $this->batchAction($actions); - $this->assertEmpty($response->getContent()); + $this->assertEquals('null', $response->getContent()); $this->assertResponseStatusCodeSame(200); $em = static::getContainer()->get(EntityManagerInterface::class); @@ -65,12 +66,17 @@ public function testAttributesBatchUpdateOK(array $actions, array $expectedAsset ksort($expectedValues); $attrAssertions = []; foreach ($expectedValues as $name => $value) { - $attrAssertions[] = [ - 'definition' => [ - 'name' => $name, - ], - 'value' => $value, - ]; + if (!is_array($value)) { + $value = [$value]; + } + foreach ($value as $v) { + $attrAssertions[] = [ + 'definition' => [ + 'name' => $name, + ], + 'value' => $v, + ]; + } } $asset = $em->getRepository(Asset::class)->findOneBy([ @@ -97,9 +103,16 @@ private function batchAction(array $actions): ResponseInterface $client = static::createClient(); $em = static::getContainer()->get(EntityManagerInterface::class); + + $workspaceId = $em->getRepository(Workspace::class)->findOneBy([ + 'slug' => 'test-workspace', + ])->getId(); + $assetsIds = array_map(fn (array $r): string => $r['id'], $em->getRepository(Asset::class)->createQueryBuilder('a') ->select('a.id') ->andWhere('a.key IS NOT NULL') + ->andWhere('a.workspace = :ws') + ->setParameter('ws', $workspaceId) ->getQuery() ->getScalarResult()); @@ -108,6 +121,7 @@ private function batchAction(array $actions): ResponseInterface 'Authorization' => 'Bearer '.KeycloakClientTestMock::getJwtFor(KeycloakClientTestMock::USER_UID), ], 'json' => [ + 'workspaceId' => $workspaceId, 'actions' => $actions, 'assets' => $assetsIds, ], diff --git a/databox/api/tests/Api/CreateAssetWithAttributeTest.php b/databox/api/tests/Api/CreateAssetWithAttributeTest.php index e076dc02e..92bf140be 100644 --- a/databox/api/tests/Api/CreateAssetWithAttributeTest.php +++ b/databox/api/tests/Api/CreateAssetWithAttributeTest.php @@ -45,12 +45,17 @@ public function testAssetCreateWithAttributes(array $attributes, array $expected $attrAssertions = []; foreach ($expectedValues as $name => $value) { - $attrAssertions[] = [ - 'definition' => [ - 'name' => $name, - ], - 'value' => $value, - ]; + if (!is_array($value)) { + $value = [$value]; + } + foreach ($value as $v) { + $attrAssertions[] = [ + 'definition' => [ + 'name' => $name, + ], + 'value' => $v, + ]; + } } $this->assertJsonContains([ '@type' => 'asset', @@ -64,7 +69,7 @@ public function testAssetCreateWithAttributes(array $attributes, array $expected public function getCases(): array { return [ - [['Description' => 'Foo bar', 'Keywords' => 'KW #1'], ['Description' => 'Foo bar', 'Keywords' => ['KW #1']]], + [['Description' => 'Foo bar', 'Keywords' => ['KW #1']], ['Description' => 'Foo bar', 'Keywords' => ['KW #1']]], [['Description' => 'Foo bar', 'Keywords' => ['KW #1']], ['Description' => 'Foo bar', 'Keywords' => ['KW #1']]], [['Description' => 'Foo bar', 'Keywords' => ['KW #1', 'KW #2']], ['Description' => 'Foo bar', 'Keywords' => ['KW #1', 'KW #2']]], ]; diff --git a/databox/api/tests/Api/TagTest.php b/databox/api/tests/Api/TagTest.php index 3879c0156..c3143e366 100644 --- a/databox/api/tests/Api/TagTest.php +++ b/databox/api/tests/Api/TagTest.php @@ -16,7 +16,22 @@ public function testGetTagCollection(): void { $limit = 10; self::enableFixtures(); - $response = static::createClient()->request('GET', '/tags?limit='.$limit, [ + + $client = static::createClient(); + $client->request('GET', '/tags?limit='.$limit, [ + 'headers' => [ + 'Authorization' => 'Bearer '.KeycloakClientTestMock::getJwtFor(KeycloakClientTestMock::USER_UID), + ], + ]); + $this->assertResponseStatusCodeSame(400); + + $response = $client->request('GET', '/tags?limit='.$limit, [ + 'query' => [ + 'limit' => $limit, + 'workspace' => $this->findIriBy(Workspace::class, [ + 'slug' => 'test-workspace', + ]), + ], 'headers' => [ 'Authorization' => 'Bearer '.KeycloakClientTestMock::getJwtFor(KeycloakClientTestMock::USER_UID), ], @@ -31,7 +46,6 @@ public function testGetTagCollection(): void '@type' => 'hydra:Collection', 'hydra:totalItems' => $resultCount, 'hydra:view' => [ - '@id' => '/tags?limit='.$limit, '@type' => 'hydra:PartialCollectionView', ], ]); diff --git a/databox/api/translations/messages.en.yaml b/databox/api/translations/messages.en.yaml index 4dfb731f0..3a85b2f8f 100644 --- a/databox/api/translations/messages.en.yaml +++ b/databox/api/translations/messages.en.yaml @@ -13,6 +13,7 @@ field_type: date: Date date_time: Date & Time geo_point: Geo point + entity: Entity privacy: secret: Secret diff --git a/databox/client/package.json b/databox/client/package.json index 1911fbfb6..f85c6fb9e 100644 --- a/databox/client/package.json +++ b/databox/client/package.json @@ -40,6 +40,7 @@ "leaflet": "^1.9.4", "moment": "^2.30.1", "pusher-js": "^8.3.0", + "re-resizable": "^6.9.17", "react": "^18.2.0", "react-ace": "^10.1.0", "react-colorful": "^5.6.1", diff --git a/databox/client/src/api/asset.ts b/databox/client/src/api/asset.ts index eecf372b7..153e07c4a 100644 --- a/databox/client/src/api/asset.ts +++ b/databox/client/src/api/asset.ts @@ -12,6 +12,7 @@ export interface GetAssetOptions { url?: string; query?: string; workspaces?: string[]; + ids?: string[]; parents?: string[]; filters?: any; order?: Record; @@ -21,6 +22,7 @@ export interface GetAssetOptions { position?: string | undefined; } | undefined; + allLocales?: boolean; } export type ESDebug = { @@ -133,6 +135,8 @@ export enum AttributeBatchActionEnum { export type AttributeBatchAction = { action?: AttributeBatchActionEnum | undefined; id?: string | undefined; + ids?: string[] | undefined; + assets?: string[] | undefined; value?: any | undefined; definitionId?: string | undefined; locale?: string | undefined; @@ -143,16 +147,7 @@ export async function attributeBatchUpdate( assetId: string | string[], actions: AttributeBatchAction[] ): Promise { - actions = actions.map(a => { - if (a.action === 'delete') { - return a; - } - - return { - ...a, - origin: 'human', - }; - }); + actions = normalizeActions(actions); if (typeof assetId === 'string') { return ( @@ -170,6 +165,36 @@ export async function attributeBatchUpdate( } } +export async function workspaceAttributeBatchUpdate( + workspaceId: string, + actions: AttributeBatchAction[] +): Promise { + return ( + await apiClient.post(`/attributes/batch-update`, { + workspaceId, + actions: normalizeActions(actions), + }) + ).data; +} + +function normalizeActions( + actions: AttributeBatchAction[] +): AttributeBatchAction[] { + return actions.map(a => { + if (a.action === AttributeBatchActionEnum.Delete) { + return { + ...a, + value: undefined, + }; + } + + return { + ...a, + origin: 'human', + }; + }); +} + export async function deleteAssetAttribute(id: string): Promise { await apiClient.delete(`/attributes/${id}`); } diff --git a/databox/client/src/api/attributeEntity.ts b/databox/client/src/api/attributeEntity.ts new file mode 100644 index 000000000..ad0317ab1 --- /dev/null +++ b/databox/client/src/api/attributeEntity.ts @@ -0,0 +1,48 @@ +import apiClient from './api-client'; +import {AttributeEntity} from '../types'; +import {ApiCollectionResponse, getHydraCollection} from './hydra'; + +const attributeEntityNS = '/attribute-entities'; + +type AttributeEntityOptions = { + query?: string; + type?: string; + workspace: string; +}; + +export async function getAttributeEntities( + options: AttributeEntityOptions +): Promise> { + const res = await apiClient.get(attributeEntityNS, { + params: { + ...options, + }, + }); + + return getHydraCollection(res.data); +} + +export async function postAttributeEntity( + workspaceId: string, + data: Partial +): Promise { + const res = await apiClient.post(attributeEntityNS, { + ...data, + workspace: `/workspaces/${workspaceId}`, + }); + + return res.data; +} + +export async function putAttributeEntity( + id: string, + data: Partial +): Promise { + const res = await apiClient.put(`${attributeEntityNS}/${id}`, data); + + return res.data; +} + +export async function deleteAttributeEntity(id: string): Promise { + await apiClient.delete(`${attributeEntityNS}/${id}`); +} diff --git a/databox/client/src/api/attributes.ts b/databox/client/src/api/attributes.ts index 2b531a733..3817838cd 100644 --- a/databox/client/src/api/attributes.ts +++ b/databox/client/src/api/attributes.ts @@ -71,6 +71,7 @@ export async function getWorkspaceAttributeDefinitions( const res = await apiClient.get(attributeDefinitionNS, { params: { workspaceId, + limit: 100, }, }); @@ -93,6 +94,7 @@ export enum AttributeType { Date = 'date', DateTime = 'date_time', GeoPoint = 'geo_point', + Entity = 'entity', Html = 'html', Ip = 'ip', Json = 'json', @@ -100,4 +102,5 @@ export enum AttributeType { Text = 'text', Textarea = 'textarea', WebVtt = 'web_vtt', + Tag = 'tag', } diff --git a/databox/client/src/components/App.tsx b/databox/client/src/components/App.tsx index ede9a4adc..da9cc2676 100644 --- a/databox/client/src/components/App.tsx +++ b/databox/client/src/components/App.tsx @@ -11,7 +11,6 @@ import apiClient from '../api/api-client'; import DisplayProvider from './Media/DisplayProvider'; import uploaderClient from '../api/uploader-client'; import {ZIndex} from '../themes/zIndex'; -import AttributeFormatProvider from './Media/Asset/Attribute/Format/AttributeFormatProvider'; import {useRequestErrorHandler} from '@alchemy/api'; import {setSentryUser} from '@alchemy/core'; import {useAuth} from '@alchemy/react-auth'; @@ -39,40 +38,38 @@ const AppProxy = React.memo(() => { leftPanelOpen={leftPanelOpen} onToggleLeftPanel={toggleLeftPanel} /> - - + +
+ {leftPanelOpen && ( + ({ + width: leftPanelWidth, + flexGrow: 0, + flexShrink: 0, + height: `calc(100vh - ${menuHeight}px)`, + overflow: 'auto', + boxShadow: theme.shadows[5], + zIndex: ZIndex.leftPanel, + })} + > + + + )}
- {leftPanelOpen && ( - ({ - width: leftPanelWidth, - flexGrow: 0, - flexShrink: 0, - height: `calc(100vh - ${menuHeight}px)`, - overflow: 'auto', - boxShadow: theme.shadows[5], - zIndex: ZIndex.leftPanel, - })} - > - - - )} -
- -
+
- - +
+
diff --git a/databox/client/src/components/AssetList/AssetContextMenu.tsx b/databox/client/src/components/AssetList/AssetContextMenu.tsx index db2d37bd7..ccb959c10 100644 --- a/databox/client/src/components/AssetList/AssetContextMenu.tsx +++ b/databox/client/src/components/AssetList/AssetContextMenu.tsx @@ -7,7 +7,7 @@ import { Menu, MenuItem, } from '@mui/material'; -import {Asset, AssetOrAssetContainer} from '../../types'; +import {Asset, AssetOrAssetContainer, StateSetter} from '../../types'; import LinkIcon from '@mui/icons-material/Link'; import EditIcon from '@mui/icons-material/Edit'; import DeleteIcon from '@mui/icons-material/Delete'; @@ -23,6 +23,7 @@ import {useNavigateToModal} from '../Routing/ModalLink'; import SaveIcon from '@mui/icons-material/Save'; import {modalRoutes} from '../../routes'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; +import {ActionsContext, ReloadFunc} from './types.ts'; type Props = { anchorPosition: PopoverPosition; @@ -30,13 +31,20 @@ type Props = { asset: Asset; item: Item; onClose: () => void; + actionsContext: ActionsContext; + reload?: ReloadFunc; + setSelection?: StateSetter; }; export default function AssetContextMenu({ asset, + item, anchorPosition, anchorEl, onClose, + actionsContext, + reload, + setSelection, }: Props) { const {openModal} = useModals(); const navigateToModal = useNavigateToModal(); @@ -111,15 +119,15 @@ export default function AssetContextMenu({ invisible: true, }} > - {original && ( + {actionsContext.open && original && ( onOpen(original.id)}> - + )} - {asset.source && ( + {actionsContext.saveAs && asset.source ? ( ({ + ) : ( + '' )} {original?.file?.alternateUrls && original.file.alternateUrls.map(a => ( openUrl(a.url)}> - + @@ -148,41 +158,81 @@ export default function AssetContextMenu({ {original?.file?.url && ( - + )} - - - - - - - - - - - - + {actionsContext.edit ? ( + + + + + + + ) : ( + '' + )} + {actionsContext.edit ? ( + + + + + + + ) : ( + '' + )} - - - - - - + {actionsContext.delete ? ( + + + + + + + ) : ( + '' + )} + {actionsContext.extraActions?.map(a => { + return ( + { + onClose(); + await a.apply([item]); + if (a.reload && reload) { + reload(); + } + if (a.resetSelection && setSelection) { + setSelection([]); + } + }} + > + {a.icon ? ( + {a.icon} + ) : ( + '' + )} + {a.labels.single} + + ); + })} ); diff --git a/databox/client/src/components/AssetList/AssetList.tsx b/databox/client/src/components/AssetList/AssetList.tsx index 3ef37438a..ee65aedbf 100644 --- a/databox/client/src/components/AssetList/AssetList.tsx +++ b/databox/client/src/components/AssetList/AssetList.tsx @@ -7,8 +7,9 @@ import { } from '../../context/AssetSelectionContext'; import {Layout, layouts} from './Layouts'; import { + ActionsContext, AssetItemComponent, - CustomItemAction, + LayoutCommonProps, LayoutProps, LoadMoreFunc, OnAddToBasket, @@ -25,6 +26,9 @@ import assetClasses from './classes'; import AssetContextMenu from './AssetContextMenu'; import {PopoverPosition} from '@mui/material/Popover/Popover'; import {SelectionActionConfigProps} from './Toolbar/SelectionActions'; +import {useSelectAllKey} from '../../hooks/useSelectAllKey.ts'; +import {createDefaultActionsContext} from './actionContext.ts'; +import useUpdateEffect from '@alchemy/react-hooks/src/useUpdateEffect'; type Props = { pages: Item[][]; @@ -38,11 +42,15 @@ type Props = { reload?: ReloadFunc; onOpenDebug?: VoidFunction; searchBar?: boolean; - actions?: CustomItemAction[]; + actionsContext?: ActionsContext; + itemOverlay?: LayoutCommonProps; + subSelection?: Item[]; onSelectionChange?: OnSelectionChange; + defaultSelection?: Item[]; itemComponent?: AssetItemComponent; previewZIndex?: number; -} & SelectionActionConfigProps; +} & SelectionActionConfigProps & + LayoutCommonProps; export default function AssetList({ pages, @@ -53,10 +61,13 @@ export default function AssetList({ loadMore, reload, searchBar, + defaultSelection = [], onOpenDebug, onSelectionChange, + subSelection, itemComponent, - actions, + actionsContext = createDefaultActionsContext(), + itemOverlay, previewZIndex, layout: defaultLayout, selectionContext: @@ -65,7 +76,8 @@ export default function AssetList({ >, ...selectionActionsProps }: Props) { - const [selection, setSelectionPrivate] = React.useState([]); + const [selection, setSelectionPrivate] = + React.useState(defaultSelection); const [layout, setLayout] = React.useState( defaultLayout ?? Layout.Grid ); @@ -78,6 +90,12 @@ export default function AssetList({ anchorEl: HTMLElement | undefined; }>(null); + React.useEffect(() => { + if (subSelection) { + setSelectionPrivate(subSelection); + } + }, [subSelection]); + const setSelection = React.useMemo>(() => { if (!onSelectionChange) { return setSelectionPrivate; @@ -94,7 +112,7 @@ export default function AssetList({ }; }, [onSelectionChange, setSelectionPrivate]); - React.useEffect(() => { + useUpdateEffect(() => { setSelectionPrivate([]); }, [pages[0]]); @@ -116,30 +134,8 @@ export default function AssetList({ }; }, [listRef.current]); - React.useEffect(() => { - const handler = (e: KeyboardEvent) => { - if (e.ctrlKey && e.key === 'a') { - const activeElement = document.activeElement; - if ( - activeElement && - ['input', 'select', 'button', 'textarea'].includes( - activeElement.tagName.toLowerCase() - ) && - (activeElement as HTMLInputElement).type !== 'checkbox' - ) { - return; - } - - e.preventDefault(); - e.stopPropagation(); - setSelection(pages.flat()); - } - }; - window.addEventListener('keydown', handler); - - return () => { - window.removeEventListener('keydown', handler); - }; + useSelectAllKey(() => { + setSelection(pages.flat()); }, [pages]); const onContextMenuOpen = React.useCallback>( @@ -179,13 +175,14 @@ export default function AssetList({ const addToCurrent = useBasketStore(state => state.addToCurrent); - const onAddToBasket = React.useCallback( - (asset, e): void => { - e?.preventDefault(); - addToCurrent([asset]); - }, - [addToCurrent] - ); + const onAddToBasket = React.useMemo(() => { + if (actionsContext.basket) { + return (asset, e): void => { + e?.preventDefault(); + addToCurrent([asset]); + }; + } + }, [addToCurrent, actionsContext.basket]); return (
({ onOpenDebug={onOpenDebug} selectionContext={SelectionContext} searchBar={searchBar} - actions={actions} + actionsContext={actionsContext} {...selectionActionsProps} /> @@ -236,15 +233,19 @@ export default function AssetList({ toolbarHeight, itemComponent, previewZIndex, + itemOverlay, } as LayoutProps)} {anchorElMenu ? ( setAnchorElMenu(null)} + reload={reload} + setSelection={setSelection} /> ) : ( '' diff --git a/databox/client/src/components/AssetList/Layouts/Grid/AssetItem.tsx b/databox/client/src/components/AssetList/Layouts/Grid/AssetItem.tsx index 0f0932758..3fc9aa29d 100644 --- a/databox/client/src/components/AssetList/Layouts/Grid/AssetItem.tsx +++ b/databox/client/src/components/AssetList/Layouts/Grid/AssetItem.tsx @@ -6,16 +6,21 @@ import IconButton from '@mui/material/IconButton'; import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; import SettingsIcon from '@mui/icons-material/Settings'; import AssetThumb from '../../../Media/Asset/AssetThumb'; -import {replaceHighlight} from '../../../Media/Asset/Attribute/Attributes'; +import {replaceHighlight} from '../../../Media/Asset/Attribute/AttributeHighlights'; import AssetTagList from '../../../Media/Asset/Widgets/AssetTagList'; import AssetCollectionList from '../../../Media/Asset/Widgets/AssetCollectionList'; -import {AssetItemProps, OnPreviewToggle} from '../../types'; +import { + AssetItemProps, + ItemOverlayRenderer, + OnPreviewToggle, +} from '../../types'; import {Checkbox} from '@mui/material'; import {stopPropagation} from '../../../../lib/stdFuncs'; import AssetItemWrapper from '../AssetItemWrapper'; type Props = { onPreviewToggle?: OnPreviewToggle; + itemOverlay?: ItemOverlayRenderer; } & AssetItemProps; export default function AssetItem({ @@ -27,6 +32,7 @@ export default function AssetItem({ onPreviewToggle, onAddToBasket, itemComponent, + itemOverlay, }: Props) { const disabled = !asset.workspace; @@ -132,6 +138,11 @@ export default function AssetItem({
)} + {itemOverlay + ? itemOverlay({ + item, + }) + : ''} ); } diff --git a/databox/client/src/components/AssetList/Layouts/Grid/GridLayout.tsx b/databox/client/src/components/AssetList/Layouts/Grid/GridLayout.tsx index 14758c3cc..dd3501858 100644 --- a/databox/client/src/components/AssetList/Layouts/Grid/GridLayout.tsx +++ b/databox/client/src/components/AssetList/Layouts/Grid/GridLayout.tsx @@ -29,6 +29,7 @@ export default function GridLayout({ loadMore, itemToAsset, previewZIndex, + ...layoutProps }: LayoutProps) { const lineHeight = 26; const collLineHeight = 32; @@ -157,6 +158,7 @@ export default function GridLayout({ onAddToBasket={onAddToBasket} onOpen={onOpen} selection={selection} + {...layoutProps} /> ))} diff --git a/databox/client/src/components/AssetList/Layouts/Grid/GridPage.tsx b/databox/client/src/components/AssetList/Layouts/Grid/GridPage.tsx index 2a2b932d7..91e6b0c52 100644 --- a/databox/client/src/components/AssetList/Layouts/Grid/GridPage.tsx +++ b/databox/client/src/components/AssetList/Layouts/Grid/GridPage.tsx @@ -22,6 +22,7 @@ function GridPage({ toolbarHeight, page, itemComponent, + itemOverlay, }: Props) { return ( <> @@ -60,6 +61,7 @@ function GridPage({ > = { layout: Layout; - setLayout: StateSetter; + setLayout?: StateSetter; loading: boolean; total?: number; pages: Item[][]; reload?: ReloadFunc; onOpenDebug?: VoidFunction; selectionContext: Context>; - actions?: CustomItemAction[]; + actionsContext: ActionsContext; } & SelectionActionConfigProps; export default function SelectionActions({ @@ -80,7 +80,7 @@ export default function SelectionActions({ pages, reload, onOpenDebug, - actions, + actionsContext, noActions, selectionContext, itemLabel = 'result', @@ -123,6 +123,10 @@ export default function SelectionActions({ let canShare = false; let wsId: string | undefined = undefined; + function filterEditableAttributes(asset: Asset): boolean { + return asset.capabilities.canEditAttributes; + } + const selectedAssets = itemToAsset ? selection.map(itemToAsset) : (selection as unknown as Asset[]); @@ -174,7 +178,18 @@ export default function SelectionActions({ id: selectedAssets[0].id, }); } else { - alert('Multi edit is coming soon...'); + navigateToModal( + modalRoutes.attributesBatchEdit, + {}, + { + state: { + selection: selectedAssets + .filter(filterEditableAttributes) + .map(a => a.id), + workspaceId: selectedAssets[0].workspace.id, + }, + } + ); } }; @@ -185,16 +200,27 @@ export default function SelectionActions({ id: selectedAssets[0].id, }); } else { - alert('Multi edit attributes is coming soon...'); + navigateToModal( + modalRoutes.attributesBatchEdit, + {}, + { + state: { + selection: selectedAssets + .filter(filterEditableAttributes) + .map(a => a.id), + workspaceId: selectedAssets[0].workspace.id, + }, + } + ); } }; const download = canDownload ? () => { - openModal(ExportAssetsDialog, { - assets: selectedAssets, - }); - } + openModal(ExportAssetsDialog, { + assets: selectedAssets, + }); + } : undefined; return { @@ -294,75 +320,93 @@ export default function SelectionActions({ {!noActions ? ( <> - {isAuthenticated() ? ( + {actionsContext.basket && isAuthenticated() ? ( ) : ( '' )} - } - > - {t('asset_actions.export', 'Export')} - - } - disabled={!canEdit} - actions={[ - { - id: 'move', - label: t('asset_actions.move', 'Move'), - onClick: onMove, - disabled: !canMove, - startIcon: , - }, - { - id: 'edit_attrs', - label: t( - 'asset_actions.edit_attributes', - 'Edit attributes' - ), - onClick: onEditAttributes, - disabled: !canEditAttributes, - startIcon: , - }, - { - id: 'copy', - label: t('asset_actions.copy', 'Copy'), - onClick: onCopy, - disabled: !canShare, - startIcon: , - }, - ]} - > - {t('asset_actions.edit', 'Edit')} - - - - {actions?.map(a => { + {actionsContext.export ? ( + } + > + {t('asset_actions.export', 'Export')} + + ) : ( + '' + )} + {actionsContext.edit ? ( + } + disabled={!canEdit || (selection.length > 0 && !canEditAttributes)} + actions={[ + { + id: 'move', + label: t('asset_actions.move', 'Move'), + onClick: onMove, + disabled: !canMove, + startIcon: , + }, + { + id: 'edit_attrs', + label: t( + 'asset_actions.edit_attributes', + 'Edit attributes' + ), + onClick: onEditAttributes, + disabled: !canEditAttributes, + startIcon: , + }, + { + id: 'copy', + label: t('asset_actions.copy', 'Copy'), + onClick: onCopy, + disabled: !canShare, + startIcon: , + }, + ]} + > + {t('asset_actions.edit', 'Edit')} + + ) : ( + '' + )} + {actionsContext.share ? ( + + ) : ( + '' + )} + {actionsContext.delete ? ( + + ) : ( + '' + )} + {actionsContext.extraActions?.map(a => { return ( + + + + + + + + + + + + + ); +} diff --git a/databox/client/src/components/AttributeEditor/DefinitionsSkeleton.tsx b/databox/client/src/components/AttributeEditor/DefinitionsSkeleton.tsx new file mode 100644 index 000000000..8964eaab8 --- /dev/null +++ b/databox/client/src/components/AttributeEditor/DefinitionsSkeleton.tsx @@ -0,0 +1,11 @@ +import {ListItem, Skeleton} from '@mui/material'; + +type Props = {}; + +export default function DefinitionsSkeleton({}: Props) { + return ( + + + + ); +} diff --git a/databox/client/src/components/AttributeEditor/EditorPanel.tsx b/databox/client/src/components/AttributeEditor/EditorPanel.tsx new file mode 100644 index 000000000..57454d896 --- /dev/null +++ b/databox/client/src/components/AttributeEditor/EditorPanel.tsx @@ -0,0 +1,172 @@ +import React from 'react'; +import {Asset, AttributeDefinition, StateSetter} from '../../types.ts'; +import {Alert, Box, Tab, Tabs} from '@mui/material'; +import { + SelectedValue, + SetAttributeValue, + CreateToKeyFunc, + Values, +} from './types.ts'; +import AttributeWidget from './AttributeWidget.tsx'; +import Flag from '../Ui/Flag.tsx'; +import {NO_LOCALE} from '../Media/Asset/Attribute/AttributesEditor.tsx'; +import MultiAttributeRow from './MultiAttributeRow.tsx'; +import {useDebounce} from '@alchemy/react-hooks/src/useDebounce.ts'; +import {createWidgetOptionsFromDefinition} from '../Media/Asset/Attribute/AttributeWidget.tsx'; + +type Props = { + definition: AttributeDefinition; + valueContainer: Values; + subSelection: Asset[]; + setAttributeValue: SetAttributeValue; + inputValueInc: number; + locale: string; + setLocale: StateSetter; + createToKey: CreateToKeyFunc; + selectedValue: SelectedValue | undefined; + setSelectedValue: StateSetter; +}; + +export default function EditorPanel({ + definition, + valueContainer, + setAttributeValue, + subSelection, + inputValueInc, + locale, + setLocale, + createToKey, + selectedValue, + setSelectedValue, +}: Props) { + const disabled = false; // TODO + const inputRef = React.useRef(null); + const [proxyValue, setValue] = React.useState(); + const [currentDefinition, setCurrentDefinition] = + React.useState(definition); + const debounce = useDebounce(); + + const value = currentDefinition === definition ? proxyValue : undefined; + + React.useEffect(() => { + inputRef.current?.focus(); + }, [definition, valueContainer, inputValueInc, locale]); + + React.useEffect(() => { + setValue( + valueContainer.indeterminate[locale] + ? null + : valueContainer.values[0]?.[locale] ?? '' + ); + setCurrentDefinition(definition); + }, [definition, subSelection, inputValueInc, locale]); + + const changeHandler = React.useCallback( + (v: T | undefined) => { + setValue(v); + debounce(() => { + setAttributeValue(v); + }, 500); + }, + [setAttributeValue] + ); + + const locales = React.useMemo(() => { + if (!definition.translatable) { + return; + } + + const locales = [...(definition.locales ?? [])]; + + if ( + valueContainer.values.some(v => + Object.hasOwnProperty.call(v, NO_LOCALE) + ) + ) { + locales.push(NO_LOCALE); + } + + return locales; + }, [valueContainer, definition]); + + if (definition.translatable && locales!.length === 0) { + return ( + + No locale defined in this workspace + + ); + } + + const humanLocale = (l: string) => (l === NO_LOCALE ? `Untranslated` : l); + + const readOnly = !definition.canEdit; + + return ( + + {locales ? ( + <> + setLocale(l)} + aria-label="Locales" + sx={{ + '.MuiTab-root': { + textTransform: 'none', + }, + }} + > + {locales.map(l => ( + + + {humanLocale(l)} + + } + value={l} + /> + ))} + + + ) : ( + '' + )} + + {definition.multiple ? ( + + ) : ( + + inputRef={inputRef} + key={definition.id} + id={definition.id} + name={definition.name} + type={definition.fieldType} + indeterminate={valueContainer.indeterminate.g} + readOnly={readOnly} + isRtl={false} + value={value as T | undefined} + disabled={disabled} + required={false} + autoFocus={true} + onChange={changeHandler} + options={createWidgetOptionsFromDefinition(definition)} + /> + )} + + ); +} diff --git a/databox/client/src/components/AttributeEditor/MultiAttributeRow.tsx b/databox/client/src/components/AttributeEditor/MultiAttributeRow.tsx new file mode 100644 index 000000000..f3f38ed91 --- /dev/null +++ b/databox/client/src/components/AttributeEditor/MultiAttributeRow.tsx @@ -0,0 +1,261 @@ +import React, {useContext, useEffect, useState} from 'react'; +import {Box, Button, IconButton} from '@mui/material'; +import {FormRow} from '@alchemy/react-form'; +import {useTranslation} from 'react-i18next'; +import AddIcon from '@mui/icons-material/Add'; +import DeleteIcon from '@mui/icons-material/Delete'; +import { + MultiValueIndex, + MultiValueValue, + SelectedValue, + SetAttributeValue, + CreateToKeyFunc, + Values, +} from './types.ts'; +import {getAttributeType} from '../Media/Asset/Attribute/types'; +import {AttributeFormatterProps} from '../Media/Asset/Attribute/types/types'; +import {AttributeFormatContext} from '../Media/Asset/Attribute/Format/AttributeFormatContext.ts'; +import AttributeWidget from './AttributeWidget.tsx'; +import classNames from 'classnames'; +import {AttributeDefinition, StateSetter} from '../../types.ts'; +import {createWidgetOptionsFromDefinition} from '../Media/Asset/Attribute/AttributeWidget.tsx'; + +type Props = { + attributeDefinition: AttributeDefinition; + valueContainer: Values; + disabled: boolean; + indeterminate?: boolean; + readOnly?: boolean; + locale: string; + createToKey: CreateToKeyFunc; + setAttributeValue: SetAttributeValue; + selectedValue: SelectedValue | undefined; + setSelectedValue: StateSetter | undefined>; +}; + +export default function MultiAttributeRow({ + valueContainer, + disabled, + attributeDefinition, + readOnly, + locale, + createToKey, + setAttributeValue, + selectedValue, + setSelectedValue, +}: Props) { + const {id, name, fieldType: type} = attributeDefinition; + + const inputRef = React.useRef(null); + const {t} = useTranslation(); + const formatContext = useContext(AttributeFormatContext); + const [newValue, setNewValue] = React.useState(); + const definitionRef = React.useRef(id); + + const addValueHandler = React.useCallback( + (v: T) => { + setAttributeValue(v, { + add: true, + updateInput: true, + }); + setNewValue(undefined); + }, + [setAttributeValue] + ); + + const addHandler = React.useCallback(() => { + addValueHandler(newValue!); + }, [addValueHandler, newValue]); + + React.useEffect(() => { + const onEnter = (e: KeyboardEvent) => { + if (e.key === 'Enter' && newValue) { + addHandler(); + } + }; + window.addEventListener('keydown', onEnter); + + return () => { + window.removeEventListener('keydown', onEnter); + }; + }, [addHandler]); + + const removeValueHandler = React.useCallback( + (v: T) => { + setAttributeValue(v, { + remove: true, + updateInput: true, + }); + }, + [setAttributeValue] + ); + + const changeNewItemHandler = React.useCallback( + (v: T | undefined) => { + setNewValue(v); + }, + [setNewValue] + ); + + const computed = React.useMemo[]>(() => { + const index: MultiValueIndex = {}; + const length = valueContainer.values.length; + const toKey = createToKey(attributeDefinition.fieldType); + + valueContainer.values.forEach(translations => { + const values = translations[locale]; + + values?.forEach((v: T) => { + const k = toKey(v); + + index[k] ??= { + p: 0, + v, + }; + index[k].p++; + }); + }); + + return Object.keys(index) + .map((k: string) => { + const v = index[k]!; + + return { + key: k, + part: Math.round((v.p / length) * 10000) / 100, + value: v.v, + }; + }) + .sort((a, b) => a.part - b.part) + .sort((a, b) => a.key.localeCompare(b.key)); + }, [valueContainer, locale, attributeDefinition]); + + const [values, setValues] = useState[]>(computed ?? []); + + const finalValues = definitionRef.current !== id ? computed ?? [] : values; + const finalNewValue = definitionRef.current !== id ? undefined : newValue; + const finalSelectedValue = + definitionRef.current !== id ? undefined : selectedValue; + + useEffect(() => { + setValues(computed ?? []); + setNewValue(undefined); + definitionRef.current = id; + }, [computed, id]); + + React.useEffect(() => { + setSelectedValue(undefined); + }, []); + + const formatter = getAttributeType(type); + const itemClassName = 'item'; + + return ( + + + id={id} + key={id} + name={name} + inputRef={inputRef} + type={type} + isRtl={false} + disabled={disabled} + required={false} + autoFocus={true} + value={finalNewValue} + onChange={changeNewItemHandler} + options={createWidgetOptionsFromDefinition(attributeDefinition)} + /> + + + {finalValues.map((v: MultiValueValue, i: number) => { + const valueFormatterProps: AttributeFormatterProps = { + value: v.value, + locale, + format: formatContext.formats[type], + }; + + const indeterminate = v.part < 100; + const isSelected = finalSelectedValue?.key === v.key; + + return ( + + setSelectedValue(p => + p && p.key === v.key + ? undefined + : { + value: v.value, + key: v.key, + } + ) + } + className={classNames({ + [itemClassName]: true, + indeterminate, + selected: isSelected, + })} + > +
+ {formatter.formatValue(valueFormatterProps)} +
+
+ { + e.stopPropagation(); + addValueHandler(v.value); + }} + color="success" + > + + + { + e.stopPropagation(); + removeValueHandler(v.value); + }} + color="error" + > + + +
+
+ ); + })} +
+ ); +} diff --git a/databox/client/src/components/AttributeEditor/PartPercentage.tsx b/databox/client/src/components/AttributeEditor/PartPercentage.tsx new file mode 100644 index 000000000..1d34160fb --- /dev/null +++ b/databox/client/src/components/AttributeEditor/PartPercentage.tsx @@ -0,0 +1,71 @@ +import {styled} from '@mui/material/styles'; + +const Label = styled('div')(({theme}) => ({ + position: 'absolute', + transform: 'translateY(-50%)', + top: '50%', + right: 0, + paddingLeft: theme.spacing(1), + color: theme.palette.primary.main, +})); + +const Container = styled('div')(({theme}) => ({ + position: 'relative', + height: 15, + marginLeft: theme.spacing(2), + fontSize: 12, +})); + +const ProgressContainer = styled('div')(() => ({ + position: 'relative', + height: '100%', +})); + +const Progress = styled('div')(({theme}) => ({ + height: '100%', + backgroundColor: theme.palette.primary.main, +})); + +export const partPercentageClassName = 'part-percent'; + +type Props = { + part: number; + width: number; + displayPercents: boolean; + total: number; +}; + +export default function PartPercentage({ + part, + width, + displayPercents, + total, +}: Props) { + const textOffset = displayPercents ? 45 : 70; + + return ( + + + + + + + ); +} diff --git a/databox/client/src/components/AttributeEditor/Resizable.tsx b/databox/client/src/components/AttributeEditor/Resizable.tsx new file mode 100644 index 000000000..fbff5b739 --- /dev/null +++ b/databox/client/src/components/AttributeEditor/Resizable.tsx @@ -0,0 +1,62 @@ +import React, {PropsWithChildren} from 'react'; + +type Props = PropsWithChildren<{ + defaultWidth: number; + minWidth?: number; + maxWidth?: number; +}> & + React.HTMLAttributes; + +export default function Resizable({ + children, + defaultWidth, + minWidth, + maxWidth, + style, + ...props +}: Props) { + const ref = React.useRef(null); + const width = React.useRef(defaultWidth); + + React.useEffect(() => { + const onMouseDown = (e: MouseEvent) => { + const t = e.currentTarget as HTMLDivElement; + const x = e.clientX; + const w = t.clientWidth; + + const onMouseMove = (e: MouseEvent) => { + width.current = w + x - e.clientX; + t.style.width = `${width.current}px`; + }; + + const onMouseUp = (_e: MouseEvent) => { + ref.current?.removeEventListener('mousemove', onMouseMove); + ref.current?.removeEventListener('mouseup', onMouseUp); + }; + + t.addEventListener('mousemove', onMouseMove); + window.document.addEventListener('mouseup', onMouseUp); + }; + + ref.current!.addEventListener('mousedown', onMouseDown); + + return () => { + ref.current?.removeEventListener('mousedown', onMouseDown); + ref.current?.removeEventListener('mousedown', onMouseDown); + }; + }, [width, ref]); + + return ( +
+ {children} +
+ ); +} diff --git a/databox/client/src/components/AttributeEditor/SavePreviewDialog.tsx b/databox/client/src/components/AttributeEditor/SavePreviewDialog.tsx new file mode 100644 index 000000000..d63266b37 --- /dev/null +++ b/databox/client/src/components/AttributeEditor/SavePreviewDialog.tsx @@ -0,0 +1,97 @@ +import {AppDialog} from '@alchemy/phrasea-ui'; +import type {StackedModalProps} from '@alchemy/navigation'; +import {useModals} from '@alchemy/navigation'; +import {useTranslation} from 'react-i18next'; +import ValueDiff, {ValueDiffProps} from './ValueDiff.tsx'; +import {workspaceAttributeBatchUpdate} from '../../api/asset.ts'; +import React from 'react'; +import {Button} from '@mui/material'; +import {LoadingButton} from '@mui/lab'; +import {FormError} from '@alchemy/react-form'; +import {getApiResponseError} from '@alchemy/api'; +import {getAttributeType} from '../Media/Asset/Attribute/types'; + +type Props = { + workspaceId: string; + onSaved: () => void; +} & ValueDiffProps & + StackedModalProps; + +export default function SavePreviewDialog({ + open, + modalIndex, + workspaceId, + actions, + onSaved, + definitionIndex, + ...props +}: Props) { + const {t} = useTranslation(); + const {closeModal} = useModals(); + const [saving, setSaving] = React.useState(false); + const [error, setError] = React.useState(); + + const doSave = async () => { + if (actions.length > 0) { + setSaving(true); + try { + await workspaceAttributeBatchUpdate( + workspaceId, + actions.map(a => { + const widget = getAttributeType( + definitionIndex[a.definitionId!].fieldType + ); + + return { + ...a, + value: widget.normalize(a.value), + }; + }) + ); + closeModal(); + onSaved(); + } catch (e: any) { + const err = getApiResponseError(e); + if (err) { + setError(err); + } else { + throw e; + } + } finally { + setSaving(false); + } + } + }; + + return ( + ( + <> + + + {t('common.save', 'Save')} + + + )} + > + + {error ? {error} : ''} + + ); +} diff --git a/databox/client/src/components/AttributeEditor/Suggestions/Preview.tsx b/databox/client/src/components/AttributeEditor/Suggestions/Preview.tsx new file mode 100644 index 000000000..2461da33d --- /dev/null +++ b/databox/client/src/components/AttributeEditor/Suggestions/Preview.tsx @@ -0,0 +1,130 @@ +import {SuggestionTabProps} from '../types.ts'; +import React from 'react'; +import {useTranslation} from 'react-i18next'; +import {Asset} from '../../../types.ts'; +import FilePlayer from '../../Media/Asset/FilePlayer.tsx'; +import {Box, IconButton, Typography} from '@mui/material'; +import {useElementResize} from '@alchemy/react-hooks/src/useElementResize'; +import KeyboardArrowLeftIcon from '@mui/icons-material/KeyboardArrowLeft'; +import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight'; + +type Props = {} & SuggestionTabProps; + +export default function Preview({ + subSelection, + defaultPanelWidth, +}: Props) { + const {t} = useTranslation(); + const [asset, setAsset] = React.useState( + subSelection[0] + ); + const containerRef = React.useRef(null); + const size = useElementResize(containerRef.current); + const finalWidth = size ? size.width : defaultPanelWidth; + const index: number = asset + ? subSelection.findIndex(i => i.id === asset.id) + : -1; + + const goTo = React.useCallback( + (offset: number) => { + setAsset(p => { + if (!p) { + return subSelection[0]; + } + + const index = subSelection.findIndex(i => i.id === p.id); + + return subSelection[ + (index + subSelection.length + offset) % subSelection.length + ]; + }); + }, + [subSelection] + ); + + React.useEffect(() => { + setAsset(subSelection[0]); + }, [subSelection]); + + return ( +
+ {asset ? ( + <> + + {asset.preview ? ( + + ) : ( + '' + )} + + + + {asset.resolvedTitle} + + + {index >= 0 ? ( + ({ + 'p': 1, + 'position': 'absolute', + 'zIndex': theme.zIndex.speedDial, + 'bottom': 0, + 'width': '100%', + 'display': 'flex', + 'flexDirection': 'row', + 'alignItems': 'center', + 'justifyContent': 'center', + '> *': { + mx: 1, + }, + })} + > + goTo(-1)}> + + +
{`${index + 1} / ${subSelection.length}`}
+ goTo(1)}> + + +
+ ) : ( + '' + )} + + ) : ( + <> + {t( + 'attribute.editor.tabs.preview.no_asset', + 'No asset selected' + )} + + )} +
+ ); +} diff --git a/databox/client/src/components/AttributeEditor/Suggestions/SuggestionPanel.tsx b/databox/client/src/components/AttributeEditor/Suggestions/SuggestionPanel.tsx new file mode 100644 index 000000000..21bb95ca7 --- /dev/null +++ b/databox/client/src/components/AttributeEditor/Suggestions/SuggestionPanel.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import {SuggestionTabProps} from '../types.ts'; +import ValuesSuggestions from './ValuesSuggestions.tsx'; +import Tabs from '../../Ui/Tabs.tsx'; +import {TabItem} from '../../Dialog/Tabbed/tabTypes.ts'; +import {useTranslation} from 'react-i18next'; +import Preview from './Preview.tsx'; +import {Optional} from '../../../utils/types.ts'; + +type Props = {} & Optional< + SuggestionTabProps, + 'definition' | 'valueContainer' +>; + +export default function SuggestionPanel(props: Props) { + const {t} = useTranslation(); + const [tab, setTab] = React.useState('preview'); + + const tabs = React.useMemo>[]>(() => { + return [ + { + id: 'preview', + component: Preview, + title: t('attribute.editor.tabs.preview.title', 'Preview'), + enabled: props.subSelection.length > 0, + }, + { + id: 'values', + component: ValuesSuggestions, + title: t('attribute.editor.tabs.values.title', 'Values'), + enabled: !!props.definition && !!props.valueContainer, + }, + ]; + }, [!props.definition, !!props.valueContainer]); + + return ( + <> + > + tabs={tabs} + currentTabId={tab} + onTabChange={id => setTab(id)} + {...(props as SuggestionTabProps)} + /> + + ); +} diff --git a/databox/client/src/components/AttributeEditor/Suggestions/ValuesSuggestions.tsx b/databox/client/src/components/AttributeEditor/Suggestions/ValuesSuggestions.tsx new file mode 100644 index 000000000..8f9cb1bb3 --- /dev/null +++ b/databox/client/src/components/AttributeEditor/Suggestions/ValuesSuggestions.tsx @@ -0,0 +1,270 @@ +import {SuggestionTabProps} from '../types.ts'; +import { + Box, + Checkbox, + InputLabel, + ListItem, + ListItemButton, + ListItemIcon, + Menu, + Stack, +} from '@mui/material'; +import React, {MouseEventHandler} from 'react'; +import {useTranslation} from 'react-i18next'; +import PartPercentage, {partPercentageClassName} from '../PartPercentage.tsx'; +import {getAttributeType} from '../../Media/Asset/Attribute/types'; +import MenuItem from '@mui/material/MenuItem'; +import EditIcon from '@mui/icons-material/Edit'; +import HighlightAltIcon from '@mui/icons-material/HighlightAlt'; +import {NO_LOCALE} from '../../Media/Asset/Attribute/AttributesEditor.tsx'; + +type Stats = Record; + +type Value = { + label: string; + value: T; + part: number; +}; +type Props = {} & SuggestionTabProps; + +export default function ValuesSuggestions({ + valueContainer, + setAttributeValue, + definition, + locale, + createToKey, + assets, + subSelection, + setSubSelection, +}: Props) { + const {t} = useTranslation(); + const [useOriginal, setUseOriginal] = React.useState(false); + const [displayPercents, setDisplayPercents] = React.useState(false); + const [anchorEl, setAnchorEl] = React.useState(); + const open = Boolean(anchorEl); + const handleClose = () => { + setAnchorEl(null); + }; + const toggleDisplayPercents = React.useCallback< + MouseEventHandler + >(e => { + e.stopPropagation(); + setDisplayPercents(p => !p); + }, []); + + const {distinctValues, lengthRef} = React.useMemo<{ + distinctValues: Value[]; + lengthRef: number; + }>(() => { + const stats: Stats = {}; + const tmpValues = ( + (!useOriginal + ? valueContainer.values + : valueContainer.originalValues) ?? [] + ).map(tr => tr[locale]); + const values = definition.multiple + ? (tmpValues.flat() as T[]) + : tmpValues; + const lengthRef = definition.multiple + ? subSelection.length + : tmpValues.length; + if (!definition.multiple && values.length === 0) { + values.push(undefined); + } + + const norm = createToKey(definition.fieldType); + const sortFn = (a: Value, b: Value) => { + if (a.part === b.part) { + return a.label + ? b.label + ? a.label.localeCompare(b.label) + : 1 + : -1; + } + + return (b.part ?? 0) - (a.part ?? 0); + }; + + const distinctValues = values + .map((v: T): Value => { + const key = norm(v); + + return { + label: key, + value: v, + part: 0, + }; + }) + .map(v => { + stats[v.label] ??= 0; + stats[v.label]++; + + return v; + }) + .map( + (v: Value): Value => ({ + ...v, + part: Math.min( + 100, + Math.round( + (stats[v.label] / (lengthRef || 1)) * 10000 + ) / 100 + ), + }) + ) + .filter( + (value, index, array) => + array.findIndex(v => v.label === value.label) === index + ) + .sort(sortFn); + return {distinctValues, lengthRef}; + }, [valueContainer, useOriginal, definition, locale]); + + const selectAssetsWithValue = React.useCallback( + (value: T) => { + const toKey = createToKey(definition.fieldType); + const key = toKey(value); + + setSubSelection( + assets.filter(a => + a.attributes.some(at => { + return ( + at.definition.id === definition.id && + (at.locale ?? NO_LOCALE) === locale && + toKey(at.value) === key + ); + }) + ) + ); + }, + [locale, definition] + ); + + const emptyValueClassName = 'empty-val'; + const labelWrapperClassName = 'label-wr'; + const labelClassName = 'label-val'; + + const widget = getAttributeType(definition.fieldType); + + return ( + +
+ + + setUseOriginal(e.target.checked)} + /> + {t( + 'attribute_editor.suggestions.originalValues.label', + 'Compare original values' + )} + + +
+ + { + setAttributeValue(anchorEl!.value, { + updateInput: true, + add: definition.multiple, + }); + handleClose(); + }} + > + + + + {t( + 'attribute_editor.suggestions.menu.apply', + 'Set this value to selected assets' + )} + + { + selectAssetsWithValue(anchorEl!.value); + handleClose(); + }} + > + + + + {t( + 'attribute_editor.suggestions.menu.select_with_value', + 'Select assets having this value' + )} + + + {distinctValues.map((v: Value, index: number) => { + return ( + + { + setAnchorEl({ + anchor: e.currentTarget, + value: v.value, + }); + }} + > +
+
+ {v.value + ? widget.formatValue({ + value: v.value, + }) + : t( + 'attribute_editor.suggestions.no_value', + '- empty -' + )} +
+
+ +
+
+
+
+ ); + })} +
+ ); +} diff --git a/databox/client/src/components/AttributeEditor/ValueDiff.tsx b/databox/client/src/components/AttributeEditor/ValueDiff.tsx new file mode 100644 index 000000000..7090aee55 --- /dev/null +++ b/databox/client/src/components/AttributeEditor/ValueDiff.tsx @@ -0,0 +1,148 @@ +import { + AttributeBatchAction, + AttributeBatchActionEnum, +} from '../../api/asset.ts'; +import { + Avatar, + List, + ListItem, + ListItemAvatar, + ListItemText, + Typography, +} from '@mui/material'; +import {AttributeDefinitionIndex} from './types.ts'; +import {styled} from '@mui/material/styles'; +import NotesIcon from '@mui/icons-material/Notes'; +import AddIcon from '@mui/icons-material/Add'; +import DeleteIcon from '@mui/icons-material/Delete'; +import {getAttributeType} from '../Media/Asset/Attribute/types'; +import {AttributeFormatterProps} from '../Media/Asset/Attribute/types/types'; +import {useContext} from 'react'; +import {AttributeFormatContext} from '../Media/Asset/Attribute/Format/AttributeFormatContext.ts'; + +type Props = { + actions: AttributeBatchAction[]; + definitionIndex: AttributeDefinitionIndex; +}; + +export type {Props as ValueDiffProps}; + +export default function ValueDiff({actions, definitionIndex}: Props) { + const formatContext = useContext(AttributeFormatContext); + + const actionIcons = { + [AttributeBatchActionEnum.Delete]: , + [AttributeBatchActionEnum.Set]: , + [AttributeBatchActionEnum.Add]: , + [AttributeBatchActionEnum.Replace]: , + }; + + const indexedActions: Record = {}; + actions.forEach(a => { + indexedActions[a.definitionId!] ??= []; + indexedActions[a.definitionId!].push(a); + }); + + return ( + + {Object.keys(indexedActions).map(defId => { + const defActions = indexedActions[defId]; + const definition = definitionIndex[defId]; + const formatter = getAttributeType(definition.fieldType); + + return ( + + + {defActions.map((a, i) => { + const valueFormatterProps: AttributeFormatterProps = + { + value: a.value, + locale: a.locale, + format: formatContext.formats[ + definition.fieldType + ], + }; + + const formatted = + formatter.formatValue( + valueFormatterProps + ); + + return ( + + + + {actionIcons[a.action!]} + + + + + {a.action! === + AttributeBatchActionEnum.Delete ? ( + + { + formatted + } + + ) : a.action === + AttributeBatchActionEnum.Add ? ( + + { + formatted + } + + ) : ( + + { + formatted + } + + )} + + {` — ${a.assets!.length} assets`} + + } + /> + + ); + })} + + } + /> + + ); + })} + + ); +} + +const Rem = styled('span')(({theme}) => ({ + textDecoration: 'line-through', + color: theme.palette.error.main, +})); + +const Set = styled('span')(({theme}) => ({ + color: theme.palette.info.main, +})); + +const Add = styled('span')(({theme}) => ({ + color: theme.palette.success.main, +})); diff --git a/databox/client/src/components/AttributeEditor/attributeGroup.ts b/databox/client/src/components/AttributeEditor/attributeGroup.ts new file mode 100644 index 000000000..c5db698b9 --- /dev/null +++ b/databox/client/src/components/AttributeEditor/attributeGroup.ts @@ -0,0 +1,454 @@ +import {Asset, AttributeDefinition, StateSetter, Tag} from '../../types.ts'; +import React from 'react'; +import { + AttributeDefinitionIndex, + AttributesCommit, + AttributesHistory, + BatchAttributeIndex, + DefinitionValuesIndex, + ExtraAttributeDefinition, + LocalizedAttributeIndex, + SetAttributeValueOptions, + CreateToKeyFunc, + Values, +} from './types'; +import {NO_LOCALE} from '../Media/Asset/Attribute/AttributesEditor'; +import {computeValues} from './store/values.ts'; +import { + computeAllDefinitionsValues, + computeDefinitionValuesHandler, +} from './store/definitionValues.ts'; +import {getBatchActions} from './batchActions.ts'; +import {useModals} from '@alchemy/navigation'; +import SavePreviewDialog from './SavePreviewDialog.tsx'; +import {useDirtyFormPromptOutsideRouter} from '../Dialog/Tabbed/FormTab.tsx'; +import {useTranslation} from 'react-i18next'; +import {AttributeType} from '../../api/attributes.ts'; +import {getAttributeType} from '../Media/Asset/Attribute/types'; + +type Props = { + attributeDefinitions: AttributeDefinition[]; + assets: Asset[]; + subSelection: Asset[]; + setSubSelection: StateSetter; + definition: AttributeDefinition | undefined; + setDefinition: StateSetter; + onSaved: () => void; +}; + +export function useAttributeValues({ + attributeDefinitions, + assets, + subSelection, + setSubSelection, + definition, + setDefinition, + onSaved, +}: Props) { + const {t} = useTranslation(); + const {openModal} = useModals(); + const [inc, setInc] = React.useState(0); + const [definitionIndex, setDefinitionIndex] = + React.useState({}); + + const createToKey = React.useCallback>(fieldType => { + const type = getAttributeType(fieldType); + + return (v: any) => { + if (!v) { + return ''; + } + + return type.normalize(v)?.toString(); + }; + }, []); + + const tagDefinition = React.useMemo( + () => + ({ + id: ExtraAttributeDefinition.Tags, + fieldType: AttributeType.Tag, + name: t('tags.label', 'Tags'), + multiple: true, + canEdit: true, + translatable: false, + }) as AttributeDefinition, + [] + ); + + const {initialIndex, finalAttributeDefinitions} = React.useMemo(() => { + const index: BatchAttributeIndex = {}; + const definitionIndex: AttributeDefinitionIndex = {}; + + const finalAttributeDefinitions = [ + tagDefinition, + ...attributeDefinitions, + ]; + + attributeDefinitions.forEach(def => { + index[def.id] ??= {}; + definitionIndex[def.id] = def; + }); + index[ExtraAttributeDefinition.Tags] ??= {}; + definitionIndex[ExtraAttributeDefinition.Tags] = tagDefinition; + + assets.forEach(a => { + a.attributes.forEach(attribute => { + const definitionId = attribute.definition.id; + + // Can be undefined due to pagination + if (!definitionIndex[definitionId]) { + attributeDefinitions.push(attribute.definition); + definitionIndex[definitionId] = attribute.definition; + index[definitionId] = {}; + } + + index[definitionId][a.id] ??= {}; + const definition = definitionIndex[definitionId]; + const assetIndex = index[definitionId][a.id]; + const locale = attribute.locale ?? NO_LOCALE; + + if (definition.multiple) { + (assetIndex[locale] as T[]) ??= []; + (assetIndex[locale] as T[]).push(attribute.value); + } else { + assetIndex[locale] = attribute.value; + } + }); + + // Add tags + (index[ExtraAttributeDefinition.Tags][ + a.id + ] as LocalizedAttributeIndex) ??= { + [NO_LOCALE]: [] as Tag[], + }; + const tagList = index[ExtraAttributeDefinition.Tags][a.id][ + NO_LOCALE + ] as Tag[]; + a.tags?.forEach(t => { + tagList.push(t); + }); + }); + + setDefinitionIndex(definitionIndex); + + return { + initialIndex: index, + finalAttributeDefinitions, + }; + }, [attributeDefinitions, assets]); + + const [history, setHistory] = React.useState>({ + history: [ + { + index: initialIndex, + definition, + subSelection, + }, + ], + current: 0, + }); + + React.useEffect(() => { + // Update definition in current history + setHistory(p => { + if ( + p.current === p.history.length - 1 && + (p.history[p.current].definition !== definition || + p.history[p.current].subSelection !== subSelection) + ) { + const h = [...p.history]; + + h[p.current] = { + ...h[p.current], + subSelection, + definition, + }; + + return { + ...p, + history: h, + }; + } + + return p; + }); + }, [definition, subSelection]); + + const [index, setIndex] = + React.useState>(initialIndex); + + const initialDefinitionValues = React.useMemo< + DefinitionValuesIndex + >(() => { + return computeAllDefinitionsValues( + finalAttributeDefinitions, + subSelection, + createToKey, + index + ); + }, [initialIndex, subSelection]); + + React.useEffect(() => { + setDefinitionValues(initialDefinitionValues); + }, [subSelection]); + + const [definitionValues, setDefinitionValues] = React.useState< + DefinitionValuesIndex + >(initialDefinitionValues); + + const values = React.useMemo | undefined>(() => { + if (definition && subSelection.length) { + return computeValues( + definition, + subSelection, + index, + initialIndex, + createToKey + ); + } + }, [definition, index, subSelection]); + + const reset = React.useCallback(() => { + setIndex(initialIndex); + }, [initialIndex]); + + React.useEffect(() => { + reset(); + }, [reset]); + + const postUpdate = React.useCallback( + (np: BatchAttributeIndex) => { + setHistory(ph => ({ + history: ph.history.slice(0, ph.current + 1).concat([ + { + index: np, + subSelection, + definition, + }, + ]), + current: ph.current + 1, + })); + + const values = computeValues( + definition!, + subSelection, + np, + initialIndex, + createToKey + ); + setDefinitionValues( + computeDefinitionValuesHandler(definition!, values) + ); + }, + [definition, subSelection, initialIndex] + ); + + const setValue = React.useCallback( + ( + locale: string, + value: T | undefined, + {add, remove, updateInput}: SetAttributeValueOptions = {} + ) => { + const defId = definition!.id; + const attributeDefinition = finalAttributeDefinitions.find( + ad => ad.id === defId + )!; + + const toKey = createToKey(attributeDefinition.fieldType); + const key = value ? toKey(value) : ''; + + setIndex(p => { + const np = {...p}; + const na = {...p[defId]}; + + subSelection.forEach(a => { + const c = {...(na[a.id] ?? {})}; + + if (add) { + if (value) { + (c[locale] as T[]) = [ + ...((c[locale] ?? []) as T[]), + ]; + if ( + !(c[locale] as T[]).some(i => key === toKey(i)) + ) { + (c[locale] as T[]).push(value); + } + } + } else if (remove) { + (c[locale] as T[]) = [...((c[locale] ?? []) as T[])]; + (c[locale] as T[]) = (c[locale] as T[]).filter( + i => key !== toKey(i) + ); + } else { + c[locale] = value; + } + + na[a.id] = c; + }); + + np[defId] = na; + + postUpdate(np); + + return np; + }); + + if (updateInput) { + setInc(p => p + 1); + } + }, + [definition, subSelection, postUpdate] + ); + + const toggleValue = React.useCallback( + (assetId: string, locale: string, value: T, checked: boolean) => { + const defId = definition!.id; + const attributeDefinition = finalAttributeDefinitions.find( + ad => ad.id === defId + )!; + locale = attributeDefinition.translatable ? locale : NO_LOCALE; + + const toKey = createToKey(attributeDefinition.fieldType); + const key = value ? toKey(value) : ''; + + setIndex(p => { + const np = {...p}; + const na = {...p[defId]}; + const c = {...(na[assetId] ?? {})}; + + if (checked) { + (c[locale] as T[]) = [...((c[locale] ?? []) as T[])]; + if (!(c[locale] as T[]).some(i => key === toKey(i))) { + (c[locale] as T[]).push(value); + } + } else { + (c[locale] as T[]) = [...((c[locale] ?? []) as T[])]; + (c[locale] as T[]) = (c[locale] as T[]).filter( + i => key !== toKey(i) + ); + } + + na[assetId] = c; + np[defId] = na; + + postUpdate(np); + + return np; + }); + }, + [definition, postUpdate] + ); + + const hasValue = React.useCallback( + (asset: Asset, locale: string, key: string): boolean => { + if (definition && definition.multiple) { + locale = definition.translatable ? locale : NO_LOCALE; + const v = index[definition.id]?.[asset.id]?.[locale]; + const toKey = createToKey(definition.fieldType); + if (v) { + return (v as T[]).some(iv => toKey(iv) === key); + } + } + + return false; + }, + [index, definition] + ); + + const applyHistory = React.useCallback( + (commit: AttributesCommit) => { + const newIndex = commit.index; + setIndex(newIndex); + + const subSelection = commit.subSelection; + const definition = commit.definition; + + setSubSelection(subSelection); + setDefinition(definition); + setDefinitionValues( + computeAllDefinitionsValues( + finalAttributeDefinitions, + subSelection, + createToKey, + newIndex + ) + ); + setInc(p => p + 1); + if (definition) { + const values = computeValues( + definition!, + subSelection, + newIndex, + initialIndex, + createToKey + ); + setDefinitionValues( + computeDefinitionValuesHandler(definition!, values) + ); + } + }, + [definition, subSelection, finalAttributeDefinitions] + ); + + const undo = React.useCallback(() => { + setHistory(ph => { + const i = ph.current - 1; + applyHistory(ph.history[i]); + + return { + ...ph, + current: i, + }; + }); + }, [applyHistory]); + + const redo = React.useCallback(() => { + setHistory(ph => { + const i = ph.current + 1; + applyHistory(ph.history[i]); + + return { + ...ph, + current: i, + }; + }); + }, [applyHistory]); + + const onSave = React.useCallback<() => Promise>(async () => { + const actions = getBatchActions( + assets, + initialIndex, + index, + definitionIndex, + createToKey + ); + + openModal(SavePreviewDialog, { + actions, + definitionIndex, + workspaceId: assets[0].workspace.id, + onSaved, + }); + }, [index, initialIndex, definitionIndex, onSaved]); + + useDirtyFormPromptOutsideRouter(history.current > 0); + + return { + attributeDefinitions: finalAttributeDefinitions, + inputValueInc: inc, + values, + setValue, + hasValue, + toggleValue, + reset, + index, + definitionValues, + history, + undo: history.current > 0 ? undo : undefined, + redo: history.current < history.history.length - 1 ? redo : undefined, + onSave, + createToKey, + }; +} diff --git a/databox/client/src/components/AttributeEditor/batchActions.ts b/databox/client/src/components/AttributeEditor/batchActions.ts new file mode 100644 index 000000000..28c357be7 --- /dev/null +++ b/databox/client/src/components/AttributeEditor/batchActions.ts @@ -0,0 +1,306 @@ +import {isSame} from '../../utils/comparison'; +import { + AttributeDefinitionIndex, + BatchAttributeIndex, + DiffGroupIndex, + ExtraAttributeDefinition, + CreateToKeyFunc, + ToKeyFuncTypeScoped, +} from './types.ts'; +import {NO_LOCALE} from '../Media/Asset/Attribute/AttributesEditor.tsx'; +import {AttributeBatchAction, AttributeBatchActionEnum} from '../../api/asset'; +import {pushUnique} from '../../utils/array.ts'; +import {Asset, Attribute} from '../../types.ts'; + +export function getBatchActions( + assets: Asset[], + initialAttributes: BatchAttributeIndex, + attributes: BatchAttributeIndex, + definitions: AttributeDefinitionIndex, + createToKey: CreateToKeyFunc +): AttributeBatchAction[] { + const setGroups: DiffGroupIndex = {}; + const addGroups: DiffGroupIndex = {}; + const deleteGroups: DiffGroupIndex = {}; + + Object.keys(attributes).forEach((defId): void => { + const definition = definitions[defId]; + if (!definition.canEdit) { + return; + } + + const toKey: ToKeyFuncTypeScoped = createToKey(definition.fieldType); + const av = attributes[defId]; + Object.keys(av).forEach((assetId): void => { + const asset = assets.find(a => a.id === assetId)!; + const lv = av[assetId]; + Object.keys(lv).forEach((locale): void => { + const currValue = lv[locale]; + const initialValue = + initialAttributes[defId]?.[assetId]?.[locale]; + + if (isSame(initialValue, currValue)) { + return; + } + + if (currValue) { + if (definition.multiple) { + if ((currValue as T[]).length > 0) { + const normalizedInitialValues: T[] = + (initialValue as T[]) ?? []; + + (currValue as T[]).forEach(v => { + const key = toKey(v); + + if ( + !normalizedInitialValues.some( + (i: T) => toKey(i) === key + ) + ) { + addGroups[defId] ??= {}; + addGroups[defId][locale] ??= {}; + addGroups[defId][locale][key] ??= { + assetIds: [], + value: v, + }; + pushUnique( + addGroups[defId][locale][key].assetIds, + assetId + ); + } + }); + + deleteNonPresent( + currValue as T[], + normalizedInitialValues, + defId, + asset, + locale, + toKey, + deleteGroups + ); + } + } else { + const key = toKey(currValue); + setGroups[defId] ??= {}; + setGroups[defId][locale] ??= {}; + setGroups[defId][locale][key] ??= { + assetIds: [], + value: currValue, + }; + pushUnique( + setGroups[defId][locale][key].assetIds, + assetId + ); + } + } + }); + }); + }); + + Object.keys(initialAttributes).forEach((defId): void => { + const definition = definitions[defId]; + if (!definition.canEdit) { + return; + } + + const toKeyForType = createToKey(definition.fieldType); + const av = initialAttributes[defId]; + Object.keys(av).forEach((assetId): void => { + const asset = assets.find(a => a.id === assetId)!; + + const lv = av[assetId]; + Object.keys(lv).forEach((locale): void => { + const initialValue = lv[locale]; + if (!initialValue) { + return; + } + + const currValue = attributes?.[defId]?.[assetId]?.[locale]; + + if (definition.multiple) { + if ((initialValue as T[]).length === 0) { + return; + } + + if ( + currValue !== undefined && + (currValue as T[]).length > 0 + ) { + return; + } + + deleteNonPresent( + (currValue as T[]) ?? [], + initialValue as T[], + defId, + asset, + locale, + toKeyForType, + deleteGroups + ); + } else { + if (currValue !== undefined) { + return; + } + + const key = toKeyForType(initialValue); + deleteGroups[defId] ??= {}; + deleteGroups[defId][locale] ??= {}; + deleteGroups[defId][locale][key] ??= { + assetIds: [], + attributeIds: [], + value: initialValue, + }; + pushUnique( + deleteGroups[defId][locale][key].assetIds, + assetId + ); + + addAttributeIdsToDeleteGroup( + asset, + defId, + locale, + key, + toKeyForType, + deleteGroups + ); + } + }); + }); + }); + + return computeActionsFromGroups(setGroups, addGroups, deleteGroups); +} + +function computeActionsFromGroups( + setGroups: DiffGroupIndex, + addGroups: DiffGroupIndex, + deleteGroups: DiffGroupIndex +): AttributeBatchAction[] { + const actions: AttributeBatchAction[] = []; + + Object.keys(setGroups).forEach(defId => { + Object.keys(setGroups[defId]).forEach(locale => { + const g = setGroups[defId][locale]; + Object.keys(g).forEach(key => { + const v = g[key]; + + actions.push({ + action: AttributeBatchActionEnum.Set, + assets: v.assetIds, + definitionId: defId, + value: v.value, + locale: locale !== NO_LOCALE ? locale : undefined, + }); + }); + }); + }); + + Object.keys(addGroups).forEach(defId => { + Object.keys(addGroups[defId]).forEach(locale => { + const g = addGroups[defId][locale]; + Object.keys(g).forEach(key => { + const v = g[key]; + + actions.push({ + action: AttributeBatchActionEnum.Add, + assets: v.assetIds, + definitionId: defId, + value: v.value, + locale: locale !== NO_LOCALE ? locale : undefined, + }); + }); + }); + }); + + Object.keys(deleteGroups).forEach(defId => { + Object.keys(deleteGroups[defId]).forEach(locale => { + const g = deleteGroups[defId][locale]; + Object.keys(g).forEach(key => { + const v = g[key]; + + actions.push({ + action: AttributeBatchActionEnum.Delete, + assets: v.assetIds, + ids: v.attributeIds, + value: v.value, + definitionId: defId, + locale: locale !== NO_LOCALE ? locale : undefined, + }); + }); + }); + }); + + return actions; +} + +function deleteNonPresent( + list: T[], + referenceList: T[], + defId: string, + asset: Asset, + locale: string, + toKeyForType: ToKeyFuncTypeScoped, + deleteGroups: DiffGroupIndex +) { + referenceList.forEach(v => { + const key = toKeyForType(v); + + if (!list.some((i: T) => toKeyForType(i) === key)) { + deleteGroups[defId] ??= {}; + deleteGroups[defId][locale] ??= {}; + deleteGroups[defId][locale][key] ??= { + assetIds: [], + attributeIds: [], + value: v, + }; + pushUnique(deleteGroups[defId][locale][key].assetIds, asset.id); + + addAttributeIdsToDeleteGroup( + asset, + defId, + locale, + key, + toKeyForType, + deleteGroups + ); + } + }); +} + +function addAttributeIdsToDeleteGroup( + asset: Asset, + defId: string, + locale: string, + key: string, + toKeyForType: ToKeyFuncTypeScoped, + deleteGroups: DiffGroupIndex +) { + if (defId === ExtraAttributeDefinition.Tags) { + const attributeIds = deleteGroups[defId][locale][key].attributeIds!; + if (!attributeIds.includes(key)) { + attributeIds.push(key); + } + + return; + } + + const attributeIds = asset.attributes + .filter((a: Attribute): boolean => { + return ( + a.definition.id === defId && + (a.locale ?? NO_LOCALE) === locale && + toKeyForType(a.value) === key + ); + }) + .map(a => a.id); + + if (attributeIds.length === 0) { + throw new Error('No attribute found for action'); + } + + attributeIds.forEach((attributeId: string) => { + deleteGroups[defId][locale][key].attributeIds!.push(attributeId); + }); +} diff --git a/databox/client/src/components/AttributeEditor/shortcuts.ts b/databox/client/src/components/AttributeEditor/shortcuts.ts new file mode 100644 index 000000000..338209eea --- /dev/null +++ b/databox/client/src/components/AttributeEditor/shortcuts.ts @@ -0,0 +1,42 @@ +import React from 'react'; +import {AttributeDefinition, StateSetter} from '../../types.ts'; + +type Props = { + attributeDefinitions: AttributeDefinition[]; + setDefinition: StateSetter; +}; + +export function useTabShortcut({attributeDefinitions, setDefinition}: Props) { + React.useEffect(() => { + const onTab = (e: KeyboardEvent) => { + if (e.key === 'Tab') { + e.preventDefault(); + + setDefinition(p => { + if (p) { + const index = attributeDefinitions.findIndex( + ad => ad.id === p?.id + ); + + if (index >= 0) { + return attributeDefinitions[ + (attributeDefinitions.length + + index + + (e.shiftKey ? -1 : 1)) % + attributeDefinitions.length + ]; + } + } + + return attributeDefinitions[0]; + }); + } + }; + + window.addEventListener('keydown', onTab); + + return () => { + window.removeEventListener('keydown', onTab); + }; + }, [attributeDefinitions]); +} diff --git a/databox/client/src/components/AttributeEditor/store/definitionValues.ts b/databox/client/src/components/AttributeEditor/store/definitionValues.ts new file mode 100644 index 000000000..4abfbf947 --- /dev/null +++ b/databox/client/src/components/AttributeEditor/store/definitionValues.ts @@ -0,0 +1,114 @@ +import {Asset, AttributeDefinition} from '../../../types'; +import { + BatchAttributeIndex, + DefinitionValuesIndex, + LocalizedAttributeIndex, + CreateToKeyFunc, + Values, +} from '../types'; +import {listsAreSame} from './helper'; + +export function computeAllDefinitionsValues( + attributeDefinitions: AttributeDefinition[], + subSelection: Asset[], + createToKey: CreateToKeyFunc, + index: BatchAttributeIndex +) { + const tree: DefinitionValuesIndex = {}; + + attributeDefinitions.forEach(def => { + const defId = def.id; + const values: Values = { + definition: def, + values: [], + originalValues: [], + indeterminate: { + g: false, + }, + }; + + const allLocales: Record = {}; + + const toKey = createToKey(values.definition.fieldType); + + subSelection.forEach(a => { + function valueIsSame( + a: T | T[] | undefined, + b: T | T[] | undefined + ): boolean { + if (def.multiple) { + return listsAreSame( + (a ?? []) as T[], + (b ?? []) as T[], + toKey + ); + } + + return (a || undefined) === (b || undefined); + } + + const translations = index[defId][a.id]; + + if (translations) { + Object.keys(translations).forEach(l => { + allLocales[l] = true; + values.indeterminate[l] ??= false; + + if ( + values.values.some( + (t: LocalizedAttributeIndex) => + !valueIsSame(t[l], translations[l]) + ) + ) { + values.indeterminate[l] = true; + values.indeterminate.g = true; + } + }); + if (!values.indeterminate.g) { + values.values.push( + translations as LocalizedAttributeIndex + ); + } + } else { + values.values.push({}); + Object.keys(allLocales).forEach(l => { + values.indeterminate[l] ??= false; + + if ( + values.values.some( + (t: LocalizedAttributeIndex) => + !valueIsSame(t[l], undefined) + ) + ) { + values.indeterminate[l] = true; + values.indeterminate.g = true; + } + }); + } + }); + + tree[defId] = { + value: values.values[0] ?? {}, + indeterminate: values.indeterminate, + }; + }); + + return tree; +} + +export function computeDefinitionValuesHandler( + definition: AttributeDefinition, + values: Values +) { + return (p: DefinitionValuesIndex): DefinitionValuesIndex => { + const n = {...p}; + const indeterminate = values.indeterminate; + + n[definition.id] = { + indeterminate, + value: values.values[0], + }; + + return n; + }; +} diff --git a/databox/client/src/components/AttributeEditor/store/helper.ts b/databox/client/src/components/AttributeEditor/store/helper.ts new file mode 100644 index 000000000..5718df866 --- /dev/null +++ b/databox/client/src/components/AttributeEditor/store/helper.ts @@ -0,0 +1,22 @@ +import {ToKeyFuncTypeScoped} from '../types'; +import {normalizeList} from './normalize'; + +export function listsAreSame( + a: T[], + b: T[], + toKey: ToKeyFuncTypeScoped +): boolean { + if (a.length !== b.length) { + return false; + } + + const an = normalizeList(a, toKey); + const bn = normalizeList(b, toKey); + for (let i = 0; i < an.length; i++) { + if (an[i] !== bn[i]) { + return false; + } + } + + return true; +} diff --git a/databox/client/src/components/AttributeEditor/store/normalize.ts b/databox/client/src/components/AttributeEditor/store/normalize.ts new file mode 100644 index 000000000..cde16fc25 --- /dev/null +++ b/databox/client/src/components/AttributeEditor/store/normalize.ts @@ -0,0 +1,8 @@ +import {ToKeyFuncTypeScoped} from '../types'; + +export function normalizeList( + a: T[], + toKey: ToKeyFuncTypeScoped +): string[] { + return a.map(toKey).sort((a, b) => a.localeCompare(b)); +} diff --git a/databox/client/src/components/AttributeEditor/store/values.ts b/databox/client/src/components/AttributeEditor/store/values.ts new file mode 100644 index 000000000..f67d5ccce --- /dev/null +++ b/databox/client/src/components/AttributeEditor/store/values.ts @@ -0,0 +1,132 @@ +import {Asset, AttributeDefinition} from '../../../types.ts'; +import { + BatchAttributeIndex, + LocalizedAttributeIndex, + CreateToKeyFunc, + ToKeyFuncTypeScoped, + Values, +} from '../types.ts'; +import {listsAreSame} from './helper.ts'; + +export function computeValues( + definition: AttributeDefinition, + subSelection: Asset[], + index: BatchAttributeIndex, + initialIndex: BatchAttributeIndex, + createToKey: CreateToKeyFunc +): Values { + const values: Values = { + definition, + values: [], + originalValues: [], + indeterminate: { + g: false, + }, + }; + + const defId = definition.id; + const allLocales: Record = {}; + + const toKey: ToKeyFuncTypeScoped = createToKey( + values.definition.fieldType + ); + + subSelection.forEach(a => { + function valueIsSame( + a: T | T[] | undefined, + b: T | T[] | undefined + ): boolean { + if (values.definition.multiple) { + return listsAreSame( + (a ?? []) as T[], + (b ?? []) as T[], + (v: T) => toKey(v) + ); + } + + return (a || undefined) === (b || undefined); + } + + const translations = index[defId][a.id]; + + if (translations) { + Object.keys(translations).forEach(l => { + allLocales[l] = true; + values.indeterminate[l] ??= false; + + if ( + values.values.some( + (t: LocalizedAttributeIndex) => + !valueIsSame(t[l], translations[l]) + ) + ) { + values.indeterminate[l] = true; + values.indeterminate.g = true; + } + }); + + values.values.push( + normalizeLocaleValues( + values.definition.multiple, + translations, + toKey + ) + ); + } else { + values.values.push({}); + Object.keys(allLocales).forEach(l => { + values.indeterminate[l] ??= false; + + if ( + values.values.some( + (t: LocalizedAttributeIndex) => + !valueIsSame(t[l], undefined) + ) + ) { + values.indeterminate[l] = true; + values.indeterminate.g = true; + } + }); + } + + if (initialIndex[defId][a.id]) { + values.originalValues.push( + normalizeLocaleValues( + values.definition.multiple, + initialIndex[defId][a.id], + toKey + ) + ); + } else { + values.originalValues.push({}); + } + }); + + return values; +} + +function normalizeLocaleValues( + multiple: boolean, + index: LocalizedAttributeIndex, + toKey: ToKeyFuncTypeScoped +): LocalizedAttributeIndex { + if (!multiple) { + return index; + } + + Object.keys(index).map(locale => { + if (index[locale]) { + (index[locale] as T[]) = (index[locale] as T[]).filter( + (value, index, array) => { + const key = toKey(value); + + return ( + array.findIndex((v: T) => toKey(v) === key) === index + ); + } + ); + } + }); + + return index; +} diff --git a/databox/client/src/components/AttributeEditor/types.ts b/databox/client/src/components/AttributeEditor/types.ts new file mode 100644 index 000000000..bb6db0c59 --- /dev/null +++ b/databox/client/src/components/AttributeEditor/types.ts @@ -0,0 +1,107 @@ +import {Asset, AttributeDefinition, StateSetter} from '../../types.ts'; + +export type IndeterminateGroup = { + g: boolean; +} & LocalizedAttributeIndex; + +export type Values = { + definition: AttributeDefinition; + indeterminate: IndeterminateGroup; + values: LocalizedAttributeIndex[]; + originalValues: LocalizedAttributeIndex[]; +}; + +export type AttributeDefinitionIndex = Record; + +export type LocalizedAttributeIndex = { + [locale: string]: T | undefined; +}; + +export type AssetAttributeIndex = { + [assetId: string]: LocalizedAttributeIndex; +}; + +export type BatchAttributeIndex = { + [definitionId: string]: AssetAttributeIndex; +}; + +export type DefinitionValuesIndex = { + [definitionId: string]: { + value: LocalizedAttributeIndex; + indeterminate: IndeterminateGroup; + }; +}; + +export type SuggestionTabProps = { + defaultPanelWidth: number; + definition: AttributeDefinition; + valueContainer: Values; + setAttributeValue: SetAttributeValue; + assets: Asset[]; + subSelection: Asset[]; + setSubSelection: StateSetter; + locale: string; + createToKey: CreateToKeyFunc; +}; + +export type SetAttributeValueOptions = { + updateInput?: boolean; + add?: boolean; + remove?: boolean; +}; + +export type SetAttributeValue = ( + value: T | undefined, + options?: SetAttributeValueOptions +) => void; + +export type MultiValueValue = { + value: T; + part: number; + key: string; +}; + +export type MultiValueIndex = { + [key: string]: { + p: number; + v: T; + }; +}; + +export type CreateToKeyFunc = ( + fieldType: string +) => ToKeyFuncTypeScoped; + +export type ToKeyFuncTypeScoped = (v: T) => string; + +export type AttributesCommit = { + index: BatchAttributeIndex; + subSelection: Asset[]; + definition: AttributeDefinition | undefined; +}; + +export type AttributesHistory = { + current: number; + history: AttributesCommit[]; +}; + +export type DiffGroupIndex = { + [definitionId: string]: { + [locale: string]: { + [key: string]: { + assetIds: string[]; + value: T; + attributeIds?: string[]; + }; + }; + }; +}; + +export enum ExtraAttributeDefinition { + Tags = 'tags', +} + +export type SelectedValue = { + value: T; + key: string; +}; diff --git a/databox/client/src/components/AttributeEntity/CreateAttributeEntityDialog.tsx b/databox/client/src/components/AttributeEntity/CreateAttributeEntityDialog.tsx new file mode 100644 index 000000000..d1d23e00c --- /dev/null +++ b/databox/client/src/components/AttributeEntity/CreateAttributeEntityDialog.tsx @@ -0,0 +1,137 @@ +import {AttributeEntity, Workspace} from '../../types.ts'; +import {useTranslation} from 'react-i18next'; +import {AppDialog} from '@alchemy/phrasea-ui'; +import { + StackedModalProps, + useInRouterDirtyFormPrompt, + useModals, +} from '@alchemy/navigation'; +import {Button, TextField} from '@mui/material'; +import {LoadingButton} from '@mui/lab'; +import {FormFieldErrors, FormRow, KeyTranslationsWidget, getNonEmptyTranslations} from '@alchemy/react-form'; +import {postAttributeEntity} from '../../api/attributeEntity.ts'; +import {toast} from 'react-toastify'; +import {useFormSubmit} from '@alchemy/api'; +import RemoteErrors from '../Form/RemoteErrors.tsx'; +import Flag from "../Ui/Flag.tsx"; +import {getWorkspace} from "../../api/workspace.ts"; +import React from "react"; + +type Props = { + value: string; + type: string; + workspaceId: string; + onCreate: (entity: AttributeEntity) => void; +} & StackedModalProps; + +export default function CreateAttributeEntityDialog({ + open, + modalIndex, + value, + type, + workspaceId, + onCreate, +}: Props) { + const {t} = useTranslation(); + const [workspace, setWorkspace] = React.useState(); + const {closeModal} = useModals(); + const formId = 'attr-entity'; + + React.useEffect(() => { + getWorkspace(workspaceId).then(w => setWorkspace(w)); + }, [workspaceId]); + + const { + submitting, + remoteErrors, + forbidNavigation, + handleSubmit, + register, + formState: {errors}, + } = useFormSubmit({ + defaultValues: { + value, + }, + onSubmit: async data => { + const d = { + ...data, + translations: getNonEmptyTranslations(data.translations ?? {}) + }; + + return await postAttributeEntity(workspaceId, { + ...d, + type, + }); + }, + onSuccess: data => { + onCreate(data); + + toast.success( + t('attribute_entity.form.created', 'Item created!') as string + ); + closeModal(); + }, + }); + + useInRouterDirtyFormPrompt(t, forbidNavigation); + + return ( + ( + <> + + + {t('common.save', 'Save')} + + + )} + > +
+ + + + + + + { + return + }} + locales={workspace?.enabledLocales ?? []} + name={'translations'} + errors={errors} + register={register} + /> + + + + +
+ ); +} diff --git a/databox/client/src/components/Basket/BasketMenuItem.tsx b/databox/client/src/components/Basket/BasketMenuItem.tsx index d3ddd9674..2466175b6 100644 --- a/databox/client/src/components/Basket/BasketMenuItem.tsx +++ b/databox/client/src/components/Basket/BasketMenuItem.tsx @@ -10,7 +10,7 @@ import EditIcon from '@mui/icons-material/Edit'; import DeleteIcon from '@mui/icons-material/Delete'; import {Basket} from '../../types'; import {useTranslation} from 'react-i18next'; -import {replaceHighlight} from '../Media/Asset/Attribute/Attributes.tsx'; +import {replaceHighlight} from '../Media/Asset/Attribute/AttributeHighlights.tsx'; import {Classes} from '../../classes.ts'; type Props = { diff --git a/databox/client/src/components/Basket/BasketViewDialog.tsx b/databox/client/src/components/Basket/BasketViewDialog.tsx index a4bf2dfe2..9ef5019d9 100644 --- a/databox/client/src/components/Basket/BasketViewDialog.tsx +++ b/databox/client/src/components/Basket/BasketViewDialog.tsx @@ -21,9 +21,10 @@ import BasketsPanel from './BasketsPanel'; import {leftPanelWidth} from '../../themes/base'; import {ZIndex} from '../../themes/zIndex'; import Box from '@mui/material/Box'; -import {OnOpen} from '../AssetList/types'; +import {ActionsContext, OnOpen} from '../AssetList/types'; import {modalRoutes} from '../../routes'; import BasketItem from './BasketItem'; +import {createDefaultActionsContext} from '../AssetList/actionContext.ts'; type Props = {} & StackedModalProps; @@ -68,6 +69,35 @@ export default function BasketViewDialog({modalIndex, open}: Props) { loadItems(); }, [loadItems, id]); + const actionsContext = React.useMemo>(() => { + return { + ...createDefaultActionsContext(), + extraActions: [ + { + name: 'removeFromBasket', + labels: { + multi: 'Remove from basket', + single: 'Remove from basket', + }, + color: 'warning', + icon: , + buttonProps: { + variant: 'contained', + }, + reload: true, + resetSelection: true, + disabled: !data?.capabilities.canEdit, + apply: async items => { + await removeFromBasket( + id!, + items.map(i => i.id) + ); + }, + }, + ], + }; + }, [removeFromBasket]); + const itemToAsset = (item: BasketAsset) => item.asset; return ( @@ -121,29 +151,7 @@ export default function BasketViewDialog({modalIndex, open}: Props) { total={pagination.total} onOpen={onOpen} previewZIndex={ZIndex.modal + 1} - actions={[ - { - name: 'removeFromBasket', - labels: { - multi: 'Remove from basket', - single: 'Remove from basket', - }, - buttonProps: { - color: 'warning', - variant: 'contained', - startIcon: , - }, - reload: true, - resetSelection: true, - disabled: !data?.capabilities.canEdit, - apply: async items => { - await removeFromBasket( - id!, - items.map(i => i.id) - ); - }, - }, - ]} + actionsContext={actionsContext} /> diff --git a/databox/client/src/components/Dialog/Asset/EditAsset.tsx b/databox/client/src/components/Dialog/Asset/EditAsset.tsx index a5288e268..ba9c50225 100644 --- a/databox/client/src/components/Dialog/Asset/EditAsset.tsx +++ b/databox/client/src/components/Dialog/Asset/EditAsset.tsx @@ -83,6 +83,7 @@ export default function EditAsset({data, onClose, minHeight}: Props) { {t('form.asset.tags.label', 'Tags')} { - onClick?: () => void; - component: FunctionComponent; -} - -type TabItem

= ( - | TabLink - | TabComponent -) & { - title: ReactNode; - id: string; - props?: P2 & P; - enabled?: boolean; - onClick?: () => void; -}; +import {TabItem} from './tabTypes.ts'; +import Tabs from '../../Ui/Tabs.tsx'; export type DialogTabProps = { onClose: () => void; minHeight?: number | undefined; }; -type Props

= { +type Props

> = { route: RouteDefinition; routeParams?: RouteParameters; tabs: TabItem

[]; @@ -40,10 +22,10 @@ type Props

= { minHeight?: number | undefined; } & P; -export default function TabbedDialog

({ +export default function TabbedDialog

>({ route, routeParams, - tabs: configTabs, + tabs, maxWidth, minHeight, title, @@ -52,24 +34,17 @@ export default function TabbedDialog

({ const {tab} = useParams(); const navigateToModal = useNavigateToModal(); const closeModal = useCloseModal(); - const tabs = configTabs.filter(t => t.enabled); - const tabIndex = tabs.findIndex(t => t.id === tab); - const currentTab = tabIndex >= 0 ? tabs[tabIndex] : undefined; - const handleChange = (_event: React.SyntheticEvent, newValue: number) => { - if (tabs[newValue].component) { - navigateToModal(route, { - ...routeParams, - tab: tabs[newValue].id, - }); - } - }; + const onChange = React.useCallback((tabId: string) => { + navigateToModal(route, { + ...routeParams, + tab: tabId, + }); + }, []); - React.useEffect(() => { - if (!currentTab) { - closeModal(); - } - }, [currentTab, closeModal]); + const onNoTab = React.useCallback(() => { + closeModal(); + }, [closeModal]); return ( @@ -83,44 +58,15 @@ export default function TabbedDialog

({ {title} - - {tabs.map(t => { - return ( - { - e.preventDefault(); - e.stopPropagation(); - t.onClick!(); - } - : t.onClick - : undefined - } - /> - ); - })} - - {currentTab && currentTab.component - ? React.createElement(currentTab.component, { - ...rest, - ...currentTab.props, - onClose: closeModal, - minHeight, - }) - : ''} + + tabs={tabs} + currentTabId={tab} + onTabChange={onChange} + onNoTab={onNoTab} + onClose={closeModal} + minHeight={minHeight} + {...rest} + /> )} diff --git a/databox/client/src/components/Dialog/Tabbed/tabTypes.ts b/databox/client/src/components/Dialog/Tabbed/tabTypes.ts new file mode 100644 index 000000000..46cd356ff --- /dev/null +++ b/databox/client/src/components/Dialog/Tabbed/tabTypes.ts @@ -0,0 +1,22 @@ +import {FunctionComponent, ReactNode} from 'react'; +import {DialogTabProps} from './TabbedDialog'; + +interface TabLink { + component?: never; +} + +interface TabComponent

{ + onClick?: () => void; + component: FunctionComponent; +} + +export type TabItem

= ( + | TabLink + | TabComponent +) & { + title: ReactNode; + id: string; + props?: P2 & P; + enabled?: boolean; + onClick?: () => void; +}; diff --git a/databox/client/src/components/Dialog/Workspace/AttributeClassManager.tsx b/databox/client/src/components/Dialog/Workspace/AttributeClassManager.tsx index 485ea63e8..3d03ae78b 100644 --- a/databox/client/src/components/Dialog/Workspace/AttributeClassManager.tsx +++ b/databox/client/src/components/Dialog/Workspace/AttributeClassManager.tsx @@ -13,7 +13,7 @@ import DefinitionManager, { DefinitionItemProps, } from './DefinitionManager'; import {useTranslation} from 'react-i18next'; -import CheckboxWidget from '../../Form/CheckboxWidget'; +import {CheckboxWidget} from '@alchemy/react-form'; import AclForm from '../../Acl/AclForm'; import {AclPermission} from '../../Acl/acl'; import {PermissionObject} from '../../Permissions/permissions'; @@ -158,7 +158,7 @@ export default function AttributeClassManager({ itemComponent={Item} listComponent={ListItem} load={() => getWorkspaceAttributeClasses(workspace.id)} - workspaceId={workspace.id} + workspace={workspace} minHeight={minHeight} onClose={onClose} createNewItem={createNewItem} diff --git a/databox/client/src/components/Dialog/Workspace/AttributeDefinitionManager.tsx b/databox/client/src/components/Dialog/Workspace/AttributeDefinitionManager.tsx index 39cf24434..6f41ca60a 100644 --- a/databox/client/src/components/Dialog/Workspace/AttributeDefinitionManager.tsx +++ b/databox/client/src/components/Dialog/Workspace/AttributeDefinitionManager.tsx @@ -21,7 +21,7 @@ import DefinitionManager, { } from './DefinitionManager'; import {useTranslation} from 'react-i18next'; import {FormFieldErrors} from '@alchemy/react-form'; -import CheckboxWidget from '../../Form/CheckboxWidget'; +import {CheckboxWidget} from '@alchemy/react-form'; import AttributeClassSelect from '../../Form/AttributeClassSelect'; import FieldTypeSelect from '../../Form/FieldTypeSelect'; import {fieldTypesIcons} from '../../../lib/icons'; @@ -30,7 +30,7 @@ import {toast} from 'react-toastify'; function Item({ usedFormSubmit, - workspaceId, + workspace, }: DefinitionItemFormProps) { const {t} = useTranslation(); @@ -87,7 +87,7 @@ function Item({ disabled={submitting} name={'class'} control={control} - workspaceId={workspaceId} + workspaceId={workspace.id} /> @@ -218,7 +218,7 @@ export default function AttributeDefinitionManager({ itemComponent={Item} listComponent={ListItem} load={() => getWorkspaceAttributeDefinitions(workspace.id)} - workspaceId={workspace.id} + workspace={workspace} minHeight={minHeight} onClose={onClose} createNewItem={createNewItem} diff --git a/databox/client/src/components/Dialog/Workspace/AttributeEntityManager.tsx b/databox/client/src/components/Dialog/Workspace/AttributeEntityManager.tsx new file mode 100644 index 000000000..716487cdf --- /dev/null +++ b/databox/client/src/components/Dialog/Workspace/AttributeEntityManager.tsx @@ -0,0 +1,123 @@ +import {AttributeEntity, Workspace} from '../../../types'; +import { + deleteAttributeEntity, + getAttributeEntities, + postAttributeEntity, + putAttributeEntity, +} from '../../../api/attributeEntity'; +import {ListItemText, TextField,} from '@mui/material'; +import {FormFieldErrors, FormRow, KeyTranslationsWidget} from '@alchemy/react-form'; +import DefinitionManager, {DefinitionItemFormProps, DefinitionItemProps,} from './DefinitionManager'; +import {useTranslation} from 'react-i18next'; +import Flag from "../../Ui/Flag.tsx"; + +let lastType = ''; + +function Item({ + usedFormSubmit, + workspace, +}: DefinitionItemFormProps) { + const {t} = useTranslation(); + + const { + register, + submitting, + formState: {errors}, + } = usedFormSubmit; + + return ( + <> + + + + + + + + + {(workspace.enabledLocales ?? []).length > 0 ? + { + return + }} + locales={workspace.enabledLocales ?? []} + name={'translations'} + errors={errors} + register={register} + /> + : ''} + + ); +} + +function ListItem({data}: DefinitionItemProps) { + return ( + <> + + + ); +} + +function createNewItem(): Partial { + return { + value: '', + type: lastType, + translations: {}, + }; +} + +type Props = { + data: Workspace; + onClose: () => void; + minHeight?: number | undefined; +}; + +export default function AttributeEntityManager({ + data: workspace, + minHeight, + onClose, +}: Props) { + const {t} = useTranslation(); + + const handleSave = async (data: AttributeEntity) => { + if (data.id) { + return await putAttributeEntity(data.id, data); + } else { + lastType = data.type; + + return await postAttributeEntity(workspace.id, { + ...data, + }); + } + }; + + return ( + getAttributeEntities({ + workspace: workspace.id, + }).then(r => r.result)} + workspace={workspace} + minHeight={minHeight} + onClose={onClose} + createNewItem={createNewItem} + newLabel={t('attribute_entity.new.label', 'New Entity')} + handleSave={handleSave} + handleDelete={deleteAttributeEntity} + /> + ); +} diff --git a/databox/client/src/components/Dialog/Workspace/DefinitionManager.tsx b/databox/client/src/components/Dialog/Workspace/DefinitionManager.tsx index 28269adaa..0c54d44e1 100644 --- a/databox/client/src/components/Dialog/Workspace/DefinitionManager.tsx +++ b/databox/client/src/components/Dialog/Workspace/DefinitionManager.tsx @@ -33,6 +33,7 @@ import SortableList, { } from '../../Ui/Sortable/SortableList'; import {useDirtyFormPrompt} from '../Tabbed/FormTab'; import {DefaultValues} from 'react-hook-form'; +import {Workspace} from "../../../types.ts"; type DefinitionBase = ApiHydraObjectResponse & {id: string}; @@ -42,7 +43,7 @@ export type DefinitionItemProps = { export type DefinitionItemFormProps = { usedFormSubmit: UseFormSubmitReturn; - workspaceId: string; + workspace: Workspace; } & DefinitionItemProps; type ListState = { @@ -99,7 +100,7 @@ type Props = { newLabel: string; handleSave: (data: D) => Promise; handleDelete?: (id: string) => Promise; - workspaceId: string; + workspace: Workspace; onSort?: OnSort; normalizeData?: (data: D) => D; }; @@ -115,7 +116,7 @@ export default function DefinitionManager({ minHeight, newLabel, handleSave, - workspaceId, + workspace, onSort, normalizeData, }: Props) { @@ -259,6 +260,10 @@ export default function DefinitionManager({ item: undefined, list: (p.list || []).filter(i => i.id !== item.id), })); + setItemState({ + item: undefined, + loading: false, + }); handleDelete(item.id); } } @@ -420,7 +425,7 @@ export default function DefinitionManager({ data: item === 'new' ? (newItem as D) : item!, key: item === 'new' ? 'new' : item!.id, usedFormSubmit, - workspaceId, + workspace, })} )} diff --git a/databox/client/src/components/Dialog/Workspace/RenditionClassManager.tsx b/databox/client/src/components/Dialog/Workspace/RenditionClassManager.tsx index f708e633b..107571b42 100644 --- a/databox/client/src/components/Dialog/Workspace/RenditionClassManager.tsx +++ b/databox/client/src/components/Dialog/Workspace/RenditionClassManager.tsx @@ -13,7 +13,7 @@ import { postRenditionClass, putRenditionClass, } from '../../../api/rendition'; -import CheckboxWidget from '../../Form/CheckboxWidget'; +import {CheckboxWidget} from '@alchemy/react-form'; import RenditionClassPermissions from './RenditionClassPermissions'; function Item({ @@ -107,7 +107,7 @@ export default function RenditionClassManager({ itemComponent={Item} listComponent={ListItem} load={() => getRenditionClasses(workspace.id).then(r => r.result)} - workspaceId={workspace.id} + workspace={workspace} minHeight={minHeight} onClose={onClose} createNewItem={createNewItem} diff --git a/databox/client/src/components/Dialog/Workspace/RenditionDefinitionManager.tsx b/databox/client/src/components/Dialog/Workspace/RenditionDefinitionManager.tsx index a73a150f8..d3ff27527 100644 --- a/databox/client/src/components/Dialog/Workspace/RenditionDefinitionManager.tsx +++ b/databox/client/src/components/Dialog/Workspace/RenditionDefinitionManager.tsx @@ -15,7 +15,7 @@ import { putRenditionDefinition, } from '../../../api/rendition'; import RenditionClassSelect from '../../Form/RenditionClassSelect'; -import CheckboxWidget from '../../Form/CheckboxWidget'; +import {CheckboxWidget} from '@alchemy/react-form'; import apiClient from '../../../api/api-client'; import {toast} from 'react-toastify'; import React from 'react'; @@ -29,7 +29,7 @@ function Item({ reset, formState: {errors}, }, - workspaceId, + workspace, }: DefinitionItemFormProps) { const {t} = useTranslation(); @@ -56,7 +56,7 @@ function Item({ disabled={submitting} name={'class'} control={control} - workspaceId={workspaceId} + workspaceId={workspace.id} /> @@ -189,7 +189,7 @@ export default function RenditionDefinitionManager({ itemComponent={Item} listComponent={ListItem} load={() => getWorkspaceRenditionDefinitions(workspace.id)} - workspaceId={workspace.id} + workspace={workspace} minHeight={minHeight} onClose={onClose} createNewItem={createNewItem} diff --git a/databox/client/src/components/Dialog/Workspace/TagManager.tsx b/databox/client/src/components/Dialog/Workspace/TagManager.tsx index 11554c31b..18bef8a90 100644 --- a/databox/client/src/components/Dialog/Workspace/TagManager.tsx +++ b/databox/client/src/components/Dialog/Workspace/TagManager.tsx @@ -150,7 +150,7 @@ export default function TagManager({ }).then(r => r.result) } loadItem={id => getTag(id)} - workspaceId={workspace.id} + workspace={workspace} minHeight={minHeight} onClose={onClose} createNewItem={createNewItem} diff --git a/databox/client/src/components/Dialog/Workspace/WorkspaceDialog.tsx b/databox/client/src/components/Dialog/Workspace/WorkspaceDialog.tsx index 890289b53..a356149b2 100644 --- a/databox/client/src/components/Dialog/Workspace/WorkspaceDialog.tsx +++ b/databox/client/src/components/Dialog/Workspace/WorkspaceDialog.tsx @@ -16,6 +16,7 @@ import RenditionDefinitionManager from './RenditionDefinitionManager'; import InfoWorkspace from './InfoWorkspace'; import {modalRoutes} from '../../../routes'; import {useCloseModal} from '../../Routing/ModalLink'; +import AttributeEntityManager from "./AttributeEntityManager.tsx"; type Props = {}; @@ -92,6 +93,18 @@ export default function WorkspaceDialog({}: Props) { }, enabled: data.capabilities.canEdit, }, + { + title: t( + 'workspace.manage.attribute_entity.title', + 'Entities' + ), + component: AttributeEntityManager, + id: 'attribute-entity', + props: { + data, + }, + enabled: data.capabilities.canEdit, + }, { title: t( 'workspace.manage.attribute_class.title', diff --git a/databox/client/src/components/Form/AttributeEntitySelect.tsx b/databox/client/src/components/Form/AttributeEntitySelect.tsx new file mode 100644 index 000000000..aafc446eb --- /dev/null +++ b/databox/client/src/components/Form/AttributeEntitySelect.tsx @@ -0,0 +1,88 @@ +import {AttributeEntity} from '../../types'; +import {FieldValues} from 'react-hook-form'; +import { + AsyncRSelectProps, + AsyncRSelectWidget, + RSelectOnCreate, + SelectOption, +} from '@alchemy/react-form'; +import {WorkspaceContext} from '../../context/WorkspaceContext.tsx'; +import React from 'react'; +import {getAttributeEntities} from '../../api/attributeEntity.ts'; +import {useModals} from '@alchemy/navigation'; +import CreateAttributeEntityDialog from '../AttributeEntity/CreateAttributeEntityDialog.tsx'; + +type Props = { + workspaceId?: string; + multiple: IsMulti; + allowNew?: boolean; + type: string; +} & AsyncRSelectProps; + +export default function AttributeEntitySelect< + TFieldValues extends FieldValues, + IsMulti extends boolean, +>({ + workspaceId: wsId, + multiple, + type, + allowNew = true, + ...rest +}: Props) { + const {openModal} = useModals(); + const workspaceContext = React.useContext(WorkspaceContext); + + const workspaceId = wsId ?? workspaceContext?.workspaceId; + if (!workspaceId) { + throw new Error('Missing workspace context'); + } + + const onCreate: RSelectOnCreate | undefined = allowNew + ? (inputValue, onCreate) => { + openModal(CreateAttributeEntityDialog, { + value: inputValue, + type, + workspaceId, + onCreate: (d: AttributeEntity) => { + console.log('d', d); + onCreate({ + label: d.value, + value: d.id, + item: d, + }); + }, + }); + } + : undefined; + + const load = async (inputValue: string): Promise => { + const data = ( + await getAttributeEntities({ + workspace: workspaceId, + type, + query: inputValue, + }) + ).result; + + return data.map((t: AttributeEntity) => ({ + value: t.id, + label: t.value, + item: t, + })); + }; + + return ( + + ); +} + +export type AttributeEntityOption = { + item: AttributeEntity; +} & SelectOption; diff --git a/databox/client/src/components/Form/TagSelect.tsx b/databox/client/src/components/Form/TagSelect.tsx index 18b9401a9..f8523808d 100644 --- a/databox/client/src/components/Form/TagSelect.tsx +++ b/databox/client/src/components/Form/TagSelect.tsx @@ -6,39 +6,58 @@ import { AsyncRSelectProps, SelectOption, } from '@alchemy/react-form'; +import {WorkspaceContext} from '../../context/WorkspaceContext.tsx'; +import React from 'react'; -type Props = { - workspaceId: string; -} & AsyncRSelectProps; +type Props = { + workspaceId?: string; + multiple: IsMulti; +} & AsyncRSelectProps; + +export default function TagSelect< + TFieldValues extends FieldValues, + IsMulti extends boolean, +>({workspaceId: wsId, multiple, ...rest}: Props) { + const workspaceContext = React.useContext(WorkspaceContext); + + const workspaceId = wsId ?? workspaceContext?.workspaceId; + if (!workspaceId) { + throw new Error('Missing workspace context'); + } -export default function TagSelect({ - workspaceId, - ...rest -}: Props) { const load = async (inputValue: string): Promise => { const data = ( await getTags({ workspace: workspaceId, + query: inputValue, }) ).result; return data - .map((t: Tag) => ({ - value: `${tagNS}/${t.id}`, - label: t.nameTranslated, - })) + .map( + (t: Tag) => + ({ + value: `${tagNS}/${t.id}`, + label: t.nameTranslated, + item: t, + }) as TagOptions + ) .filter(i => i.label.toLowerCase().includes((inputValue || '').toLowerCase()) ); }; return ( - + ); } + +export type TagOptions = { + item: Tag; +} & SelectOption; diff --git a/databox/client/src/components/Form/WorkspaceForm.tsx b/databox/client/src/components/Form/WorkspaceForm.tsx index 21df968fa..dbbed1ef0 100644 --- a/databox/client/src/components/Form/WorkspaceForm.tsx +++ b/databox/client/src/components/Form/WorkspaceForm.tsx @@ -11,7 +11,7 @@ import {SortableCollectionWidget} from '@alchemy/react-form'; import Flag from '../Ui/Flag'; import {useDirtyFormPrompt} from '../Dialog/Tabbed/FormTab'; -import CheckboxWidget from './CheckboxWidget'; +import {CheckboxWidget} from '@alchemy/react-form'; const emptyLocaleItem = ''; diff --git a/databox/client/src/components/Integration/Phrasea/Expose/CreatePublicationDialog.tsx b/databox/client/src/components/Integration/Phrasea/Expose/CreatePublicationDialog.tsx index aa661cbd9..d5dfedaea 100644 --- a/databox/client/src/components/Integration/Phrasea/Expose/CreatePublicationDialog.tsx +++ b/databox/client/src/components/Integration/Phrasea/Expose/CreatePublicationDialog.tsx @@ -12,7 +12,7 @@ import { } from '@alchemy/navigation'; import {Basket, IntegrationData} from '../../../../types.ts'; import {runIntegrationAction} from '../../../../api/integrations.ts'; -import SwitchWidget from '../../../Form/SwitchWidget.tsx'; +import {SwitchWidget} from '@alchemy/react-form'; import ExposeProfileSelect from './ExposeProfileSelect.tsx'; import {ExposePublication} from './exposeType.ts'; import ExposePublicationSelect from './ExposePublicationSelect.tsx'; diff --git a/databox/client/src/components/Media/Asset/Actions/CopyAssetsDialog.tsx b/databox/client/src/components/Media/Asset/Actions/CopyAssetsDialog.tsx index bd566d679..9f21dc3b4 100644 --- a/databox/client/src/components/Media/Asset/Actions/CopyAssetsDialog.tsx +++ b/databox/client/src/components/Media/Asset/Actions/CopyAssetsDialog.tsx @@ -9,7 +9,7 @@ import {FormFieldErrors} from '@alchemy/react-form'; import FileCopyIcon from '@mui/icons-material/FileCopy'; import RemoteErrors from '../../../Form/RemoteErrors'; import {FormRow} from '@alchemy/react-form'; -import SwitchWidget from '../../../Form/SwitchWidget'; +import {SwitchWidget} from '@alchemy/react-form'; import {Asset} from '../../../../types'; import AssetSelection from '../../../AssetList/AssetSelection'; import {StackedModalProps, useModals} from '@alchemy/navigation'; diff --git a/databox/client/src/components/Media/Asset/Annotations/AssetAnnotationsOverlay.tsx b/databox/client/src/components/Media/Asset/Annotations/AssetAnnotationsOverlay.tsx new file mode 100644 index 000000000..833f3c7e6 --- /dev/null +++ b/databox/client/src/components/Media/Asset/Annotations/AssetAnnotationsOverlay.tsx @@ -0,0 +1,42 @@ +import {AnnotationType, AssetAnnotation} from '../../../../types.ts'; +import PointAnnotation from './PointAnnotation.tsx'; +import React, {FC} from 'react'; +import RectAnnotation from './RectAnnotation.tsx'; +import CircleAnnotation from './CircleAnnotation.tsx'; + +type Props = { + annotations: AssetAnnotation[]; +}; + +export default function AssetAnnotationsOverlay({annotations}: Props) { + const types: { + [key in AnnotationType]?: FC; + } = { + [AnnotationType.Point]: PointAnnotation, + [AnnotationType.Rect]: RectAnnotation, + [AnnotationType.Circle]: CircleAnnotation, + }; + + return ( +

+ {annotations.map(({type, ...props}, i) => { + if (!types[type]) { + return ''; + } + + return React.createElement(types[type]!, { + key: i, + ...props, + }); + })} +
+ ); +} diff --git a/databox/client/src/components/Media/Asset/Annotations/CircleAnnotation.tsx b/databox/client/src/components/Media/Asset/Annotations/CircleAnnotation.tsx new file mode 100644 index 000000000..288b05b02 --- /dev/null +++ b/databox/client/src/components/Media/Asset/Annotations/CircleAnnotation.tsx @@ -0,0 +1,34 @@ +type Props = { + x: number; + y: number; + r: number; + b?: number; + c?: string; + f?: string; +}; + +export default function CircleAnnotation({ + x, + y, + r, + b = 3, + c = '#000', + f, +}: Props) { + return ( +
+ ); +} diff --git a/databox/client/src/components/Media/Asset/Annotations/PointAnnotation.tsx b/databox/client/src/components/Media/Asset/Annotations/PointAnnotation.tsx new file mode 100644 index 000000000..12cdc9ca1 --- /dev/null +++ b/databox/client/src/components/Media/Asset/Annotations/PointAnnotation.tsx @@ -0,0 +1,24 @@ +type Props = { + x: number; + y: number; + s?: number; + c?: string; +}; + +export default function PointAnnotation({x, y, s = 30, c = '#000'}: Props) { + return ( +
+ ); +} diff --git a/databox/client/src/components/Media/Asset/Annotations/RectAnnotation.tsx b/databox/client/src/components/Media/Asset/Annotations/RectAnnotation.tsx new file mode 100644 index 000000000..28e745aa3 --- /dev/null +++ b/databox/client/src/components/Media/Asset/Annotations/RectAnnotation.tsx @@ -0,0 +1,34 @@ +type Props = { + x1: number; + y1: number; + x2: number; + y2: number; + b?: number; + c?: string; + f?: string; +}; + +export default function RectAnnotation({ + x1, + y1, + x2, + y2, + b = 3, + c = '#000', + f, +}: Props) { + return ( +
+ ); +} diff --git a/databox/client/src/components/Media/Asset/AssetAttributes.tsx b/databox/client/src/components/Media/Asset/AssetAttributes.tsx new file mode 100644 index 000000000..a878fff12 --- /dev/null +++ b/databox/client/src/components/Media/Asset/AssetAttributes.tsx @@ -0,0 +1,51 @@ +import { + Accordion, + AccordionDetails, + AccordionSummary, + Box, + Typography, +} from '@mui/material'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import Attributes, { + attributesSx, + OnAnnotations, +} from './Attribute/Attributes.tsx'; +import React from 'react'; +import {Asset} from '../../../types.ts'; +import {useTranslation} from 'react-i18next'; + +type Props = { + asset: Asset; + onAnnotations: OnAnnotations | undefined; +}; + +export default function AssetAttributes({asset, onAnnotations}: Props) { + const [expanded, setExpanded] = React.useState(true); + const {t} = useTranslation(); + + return ( + + setExpanded(p => !p)} + > + } + aria-controls="attr-content" + id="attr-header" + > + + {t('asset.view.attributes', `Asset Attributes`)} + + + + + + + + ); +} diff --git a/databox/client/src/components/Media/Asset/AssetView.tsx b/databox/client/src/components/Media/Asset/AssetView.tsx index 140a854b9..889612ba4 100644 --- a/databox/client/src/components/Media/Asset/AssetView.tsx +++ b/databox/client/src/components/Media/Asset/AssetView.tsx @@ -1,5 +1,5 @@ import React, {FC, useCallback, useEffect, useMemo, useState} from 'react'; -import {Asset, AssetRendition} from '../../../types'; +import {Asset, AssetAnnotation, AssetRendition} from '../../../types'; import {AppDialog} from '@alchemy/phrasea-ui'; import FilePlayer from './FilePlayer'; import {useWindowSize} from '@alchemy/react-hooks/src/useWindowSize'; @@ -14,6 +14,10 @@ import {getAssetRenditions} from '../../../api/rendition'; import MenuItem from '@mui/material/MenuItem'; import {useCloseModal, useNavigateToModal} from '../../Routing/ModalLink'; import {modalRoutes} from '../../../routes'; +import {scrollbarWidth} from '../../../constants.ts'; +import AssetAttributes from './AssetAttributes.tsx'; +import {OnAnnotations} from './Attribute/Attributes.tsx'; +import AssetAnnotationsOverlay from './Annotations/AssetAnnotationsOverlay.tsx'; export type IntegrationOverlayCommonProps = { dimensions: Dimensions; @@ -31,17 +35,17 @@ export type SetIntegrationOverlayFunction

= ( replace?: boolean ) => void; -const menuWidth = 300; - -const headerHeight = 60; -const scrollBarDelta = 8; - type Props = {} & StackedModalProps; export default function AssetView({modalIndex}: Props) { + const menuWidth = 300; + const headerHeight = 60; const {id: assetId, renditionId} = useParams(); const navigateToModal = useNavigateToModal(); const closeModal = useCloseModal(); + const [annotations, setAnnotations] = React.useState< + AssetAnnotation[] | undefined + >(); const [data, setData] = useState(); const [renditions, setRenditions] = useState(); @@ -64,6 +68,10 @@ export default function AssetView({modalIndex}: Props) { })(); }, [assetId]); + const onAnnotations = React.useCallback(annotations => { + setAnnotations(annotations); + }, []); + const winSize = useWindowSize(); const [integrationOverlay, setIntegrationOverlay] = useState(); @@ -81,7 +89,7 @@ export default function AssetView({modalIndex}: Props) { const dimensions = useMemo(() => { return { - width: winSize.innerWidth - menuWidth - scrollBarDelta, + width: winSize.innerWidth - menuWidth - scrollbarWidth, height: winSize.innerHeight - headerHeight - 2, }; }, [winSize]); @@ -147,8 +155,8 @@ export default function AssetView({modalIndex}: Props) { sx={{ overflowY: 'auto', height: dimensions.height, - width: dimensions.width + scrollBarDelta, - maxWidth: dimensions.width + scrollBarDelta, + width: dimensions.width + scrollbarWidth, + maxWidth: dimensions.width + scrollbarWidth, }} >

+ {annotations ? ( + + ) : ( + '' + )} {rendition?.file && (!integrationOverlay || !integrationOverlay.replace) && ( @@ -187,6 +202,14 @@ export default function AssetView({modalIndex}: Props) { height: dimensions.height, })} > + {data ? ( + + ) : ( + '' + )} {rendition?.file ? ( FreeNode, + options: { + props?: {}; + depth?: number; + stopTags?: string[]; + } = {} +): FreeNode { + if (typeof text === 'string') { + return func(text); + } else if (React.isValidElement(text)) { + if ( + (options.stopTags ?? []).includes( + (text as ReactElement).type + ) + ) { + return text; + } + + return React.cloneElement( + text, + options.props || {}, + replaceText(text.props.children, func, options) + ) as ReactElement; + } else if (Array.isArray(text)) { + return text + .map((e, i) => + replaceText(e, func, { + ...options, + depth: (options.depth ?? 0) + 1, + props: { + key: `${options.depth?.toString() ?? '0'}:${i}`, + }, + }) + ) + .flat(); + } + + return text; +} + +const Highlight = styled('em')(({theme}) => ({ + backgroundColor: theme.palette.warning.main, + color: theme.palette.warning.contrastText, + padding: '1px 3px', + margin: '-1px -3px', + borderRadius: 3, +})); + +export function replaceHighlight( + value?: string, + Compoment: React.FunctionComponent> = Highlight +): FreeNode { + if (!value) { + return []; + } + + const replaced = reactStringReplace( + value, + /\[hl](.*?)\[\/hl]/g, + (m, index) => { + return {m}; + } + ); + + return replaceText(replaced, nl2br); +} diff --git a/databox/client/src/components/Media/Asset/Attribute/AttributeRowUI.tsx b/databox/client/src/components/Media/Asset/Attribute/AttributeRowUI.tsx index 9aa05f3b3..702bb826b 100644 --- a/databox/client/src/components/Media/Asset/Attribute/AttributeRowUI.tsx +++ b/databox/client/src/components/Media/Asset/Attribute/AttributeRowUI.tsx @@ -3,40 +3,33 @@ import VisibilityIcon from '@mui/icons-material/Visibility'; import {IconButton} from '@mui/material'; import {getAttributeType} from './types'; import PushPinIcon from '@mui/icons-material/PushPin'; -import CopyAttribute from './CopyAttribute'; +import CopyAttribute, {copyToClipBoardContainerClass} from './CopyAttribute'; import React from 'react'; -import {attributesClasses} from './Attributes'; +import {attributesClasses, OnAnnotations} from './Attributes'; import {isRtlLocale} from '../../../../lib/lang'; +import {Attribute, AttributeDefinition} from '../../../../types.ts'; type Props = { - type: string; - definitionId: string; - locale: string | undefined; - attributeName: string; - value: any; - highlight?: any; + definition: AttributeDefinition; + attribute: Attribute | Attribute[]; displayControls: boolean; - multiple: boolean; togglePin: (definitionId: string) => void; pinned: boolean; formatContext: TAttributeFormatContext; + onAnnotations?: OnAnnotations | undefined; }; export default function AttributeRowUI({ - type, - definitionId, - locale, - attributeName, - value, - highlight, - multiple, + definition, + attribute, togglePin, pinned, displayControls, formatContext, + onAnnotations, }: Props) { - const isRtl = isRtlLocale(locale); - const formatter = getAttributeType(type); + const {id, name, fieldType, multiple} = definition; + const formatter = getAttributeType(fieldType); const [overControls, setOverControls] = React.useState(false); const toggleFormat = React.useCallback< @@ -44,17 +37,21 @@ export default function AttributeRowUI({ >( e => { e.stopPropagation(); - formatContext.toggleFormat(type); + formatContext.toggleFormat(fieldType); }, [formatContext] ); + const locale = multiple ? undefined : (attribute as Attribute).locale; + const isRtl = locale ? isRtlLocale(locale) : false; + const valueFormatterProps = { - value, - highlight, + value: multiple + ? (attribute as Attribute[]).map(a => a.value) + : (attribute as Attribute).value, + highlight: multiple ? undefined : (attribute as Attribute).highlight, locale, - multiple, - format: formatContext.formats[type], + format: formatContext.formats[fieldType], }; return ( @@ -70,12 +67,12 @@ export default function AttributeRowUI({ onMouseLeave={() => setOverControls(false)} >
- {attributeName} + {name} {displayControls ? (
{overControls ? ( <> - {formatContext.hasFormats(type) && ( + {formatContext.hasFormats(fieldType) && ( @@ -87,9 +84,7 @@ export default function AttributeRowUI({ )} /> - togglePin(definitionId)} - > + togglePin(id)}> @@ -103,23 +98,46 @@ export default function AttributeRowUI({ '' )}
-
- {multiple && !formatter.supportsMultiple() ? ( +
+ {multiple ? (
    - {value - ? value.map((v: any, i: number) => { + {attribute + ? (attribute as Attribute[]).map((a, i: number) => { const formatProps = { - value: v, - highlight, - locale, - multiple, - format: formatContext.formats[type], + value: a.value, + highlight: a.highlight, + locale: a.locale, + format: formatContext.formats[fieldType], }; + const isRtl = isRtlLocale(a.locale); + return ( -
  • +
  • + onAnnotations( + a.assetAnnotations! + ) + : undefined + } + > {formatter.formatValue(formatProps)} - {displayControls && overControls ? ( + {displayControls ? ( @@ -97,6 +100,7 @@ export default function AttributeType({ } onChange={v => changeHandler(NO_LOCALE, v)} id={definition.id} + options={createWidgetOptionsFromDefinition(definition)} /> ) : ( changeHandler(NO_LOCALE, v)} id={definition.id} + options={createWidgetOptionsFromDefinition(definition)} /> )} diff --git a/databox/client/src/components/Media/Asset/Attribute/AttributeWidget.tsx b/databox/client/src/components/Media/Asset/Attribute/AttributeWidget.tsx index 3abb23966..8946adc22 100644 --- a/databox/client/src/components/Media/Asset/Attribute/AttributeWidget.tsx +++ b/databox/client/src/components/Media/Asset/Attribute/AttributeWidget.tsx @@ -1,6 +1,8 @@ import React, {useCallback, useEffect, useState} from 'react'; import {AttrValue, createNewValue} from './AttributesEditor'; import {getAttributeType} from './types'; +import {AttributeWidgetOptions} from './types/types'; +import {AttributeDefinition} from '../../../../types.ts'; type Props = { id: string; @@ -14,6 +16,7 @@ type Props = { autoFocus?: boolean; isRtl: boolean; onChange: (value: AttrValue) => void; + options: AttributeWidgetOptions; }; export default function AttributeWidget({ @@ -28,6 +31,7 @@ export default function AttributeWidget({ type, indeterminate, readOnly, + options, }: Props) { const denormalizeInputValue = ( initialValue: AttrValue | undefined @@ -82,7 +86,16 @@ export default function AttributeWidget({ indeterminate, autoFocus, disabled, + options, })} ); } + +export function createWidgetOptionsFromDefinition( + definition: AttributeDefinition +): AttributeWidgetOptions { + return { + type: definition.entityType, + }; +} diff --git a/databox/client/src/components/Media/Asset/Attribute/Attributes.tsx b/databox/client/src/components/Media/Asset/Attribute/Attributes.tsx index 230af557f..ee4731997 100644 --- a/databox/client/src/components/Media/Asset/Attribute/Attributes.tsx +++ b/databox/client/src/components/Media/Asset/Attribute/Attributes.tsx @@ -1,97 +1,31 @@ -import {Asset, Attribute} from '../../../../types'; -import reactStringReplace from 'react-string-replace'; -import React, { - PropsWithChildren, - ReactElement, - ReactNode, - useContext, -} from 'react'; -import {styled} from '@mui/material/styles'; +import {Asset, AssetAnnotation} from '../../../../types'; +import React, {useContext} from 'react'; import AttributeRowUI from './AttributeRowUI'; import {SxProps} from '@mui/material'; -import nl2br from 'react-nl2br'; import {stopPropagation} from '../../../../lib/stdFuncs'; import {UserPreferencesContext} from '../../../User/Preferences/UserPreferencesContext'; import {AttributeFormatContext} from './Format/AttributeFormatContext'; +import {buildAttributesGroupedByDefinition} from './attributeIndex.ts'; +import { + copyToClipBoardClass, + copyToClipBoardContainerClass, +} from './CopyAttribute.tsx'; -type FreeNode = string | ReactNode | ReactNode[]; - -function replaceText( - text: FreeNode, - func: (text: string) => FreeNode, - options: { - props?: {}; - depth?: number; - stopTags?: string[]; - } = {} -): FreeNode { - if (typeof text === 'string') { - return func(text); - } else if (React.isValidElement(text)) { - if ( - (options.stopTags ?? []).includes( - (text as ReactElement).type - ) - ) { - return text; - } - - return React.cloneElement( - text, - options.props || {}, - replaceText(text.props.children, func, options) - ) as ReactElement; - } else if (Array.isArray(text)) { - return text - .map((e, i) => - replaceText(e, func, { - ...options, - depth: (options.depth ?? 0) + 1, - props: { - key: `${options.depth?.toString() ?? '0'}:${i}`, - }, - }) - ) - .flat(); - } - - return text; -} - -const Highlight = styled('em')(({theme}) => ({ - backgroundColor: theme.palette.warning.main, - color: theme.palette.warning.contrastText, - padding: '1px 3px', - margin: '-1px -3px', - borderRadius: 3, -})); - -export function replaceHighlight( - value?: string, - Compoment: React.FunctionComponent> = Highlight -): FreeNode { - if (!value) { - return []; - } - - const replaced = reactStringReplace( - value, - /\[hl](.*?)\[\/hl]/g, - (m, index) => { - return {m}; - } - ); - - return replaceText(replaced, nl2br); -} +export type OnAnnotations = (annotations: AssetAnnotation[]) => void; type Props = { asset: Asset; displayControls: boolean; pinnedOnly?: boolean; + onAnnotations?: OnAnnotations | undefined; }; -function Attributes({asset, displayControls, pinnedOnly}: Props) { +function Attributes({ + asset, + displayControls, + pinnedOnly, + onAnnotations, +}: Props) { const {preferences, updatePreference} = useContext(UserPreferencesContext); const formatContext = useContext(AttributeFormatContext); @@ -117,24 +51,19 @@ function Attributes({asset, displayControls, pinnedOnly}: Props) { const pinnedAttributes = (preferences.pinnedAttrs ?? {})[asset.workspace.id] ?? []; - const sortedAttributes: Attribute[] = []; - pinnedAttributes.forEach(defId => { - const i = asset.attributes.findIndex(a => a.definition.id === defId); - if (i >= 0) { - sortedAttributes.push(asset.attributes[i]); - } + let attributeGroups = buildAttributesGroupedByDefinition(asset.attributes); + + attributeGroups.sort((a, b) => { + const aa = pinnedAttributes.includes(a.definition.id) ? 1 : 0; + const bb = pinnedAttributes.includes(b.definition.id) ? 1 : 0; + + return bb - aa; }); - if (!pinnedOnly) { - asset.attributes.forEach(a => { - if ( - !sortedAttributes.some( - sa => sa.definition.id === a.definition.id - ) - ) { - sortedAttributes.push(a); - } - }); + if (pinnedOnly) { + attributeGroups = attributeGroups.filter(g => + pinnedAttributes.includes(g.definition.id) + ); } return ( @@ -143,22 +72,20 @@ function Attributes({asset, displayControls, pinnedOnly}: Props) { onClick={stopPropagation} onMouseDown={stopPropagation} > - {sortedAttributes.map(a => ( - - ))} + {attributeGroups.map(g => { + return ( + + ); + })}
); } @@ -201,5 +128,12 @@ export function attributesSx(): SxProps { m: 0, pl: 1, }, + [`.${copyToClipBoardContainerClass} .${copyToClipBoardClass}`]: { + visibility: 'hidden', + ml: 2, + }, + [`.${copyToClipBoardContainerClass}:hover .${copyToClipBoardClass}`]: { + visibility: 'visible', + }, }; } diff --git a/databox/client/src/components/Media/Asset/Attribute/AttributesEditorForm.tsx b/databox/client/src/components/Media/Asset/Attribute/AttributesEditorForm.tsx index 410cbd12e..74c174832 100644 --- a/databox/client/src/components/Media/Asset/Attribute/AttributesEditorForm.tsx +++ b/databox/client/src/components/Media/Asset/Attribute/AttributesEditorForm.tsx @@ -7,6 +7,7 @@ import AttributesEditor from './AttributesEditor'; import {useAttributeEditor} from './useAttributeEditor'; import {FormRow} from '@alchemy/react-form'; import React from 'react'; +import {WorkspaceContext} from '../../../../context/WorkspaceContext.tsx'; type Props = { workspaceId: string; @@ -68,49 +69,55 @@ export default function AttributesEditorForm({ return ( <> - - {attributes && definitionIndex ? ( - - ) : ( - <> - {[0, 1, 2].map(x => ( - - - + + {attributes && definitionIndex ? ( + + ) : ( + <> + {[0, 1, 2].map(x => ( + + + + + - - - - - ))} - - )} - + + + ))} + + )} + + ); } diff --git a/databox/client/src/components/Media/Asset/Attribute/CopyAttribute.tsx b/databox/client/src/components/Media/Asset/Attribute/CopyAttribute.tsx index 493630f3f..0e7912f45 100644 --- a/databox/client/src/components/Media/Asset/Attribute/CopyAttribute.tsx +++ b/databox/client/src/components/Media/Asset/Attribute/CopyAttribute.tsx @@ -2,12 +2,13 @@ import {IconButton} from '@mui/material'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import CopyToClipboard from '../../../../lib/CopyToClipboard'; +export const copyToClipBoardClass = 'ctcb'; +export const copyToClipBoardContainerClass = 'ctcb-wr'; + type Props = { value: string | undefined; }; -export const copyToClipBoardClass = 'ctcb'; - export default function CopyAttribute({value}: Props) { return ( diff --git a/databox/client/src/components/Media/Asset/Attribute/MultiAttributeRow.tsx b/databox/client/src/components/Media/Asset/Attribute/MultiAttributeRow.tsx index 7bf0ee493..26c9dd3cb 100644 --- a/databox/client/src/components/Media/Asset/Attribute/MultiAttributeRow.tsx +++ b/databox/client/src/components/Media/Asset/Attribute/MultiAttributeRow.tsx @@ -6,6 +6,7 @@ import {FormRow} from '@alchemy/react-form'; import {useTranslation} from 'react-i18next'; import AddIcon from '@mui/icons-material/Add'; import DeleteIcon from '@mui/icons-material/Delete'; +import {AttributeWidgetOptions} from './types/types'; type Props = { id: string; @@ -17,6 +18,7 @@ type Props = { disabled: boolean; indeterminate?: boolean; readOnly?: boolean; + options: AttributeWidgetOptions; }; const deferred = 0; @@ -31,6 +33,7 @@ export default function MultiAttributeRow({ type, indeterminate, readOnly, + options, }: Props) { const {t} = useTranslation(); const [values, setValues] = useState[]>( @@ -105,6 +108,7 @@ export default function MultiAttributeRow({ changeHandler(i, v); }} id={`${id}_${i}`} + options={options} /> + + + ); + }} + + + ); +} + +const Search = styled('div')(({theme}) => ({ + 'position': 'relative', + 'borderRadius': theme.shape.borderRadius, + 'backgroundColor': alpha(theme.palette.common.white, 0.35), + '&:hover': { + backgroundColor: alpha(theme.palette.common.white, 0.65), + }, + 'display': 'flex', + 'alignItems': 'center', + 'margin': theme.spacing(1), + [theme.breakpoints.up('sm')]: { + width: 'fit-content', + }, +})); + +const SearchIconWrapper = styled('div')(({theme}) => ({ + padding: theme.spacing(0, 2), + height: '100%', + position: 'absolute', + pointerEvents: 'none', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +})); + +const StyledInputBase = styled(InputBase)(({theme}) => ({ + 'color': 'inherit', + '& .MuiInputBase-input': { + padding: theme.spacing(1, 1, 1, 0), + // vertical padding + font size from searchIcon + paddingLeft: `calc(1em + ${theme.spacing(4)})`, + width: '100%', + }, +})); diff --git a/databox/client/src/components/Media/Search/SearchBar.tsx b/databox/client/src/components/Media/Search/SearchBar.tsx index bf686af36..8cb23ce8f 100644 --- a/databox/client/src/components/Media/Search/SearchBar.tsx +++ b/databox/client/src/components/Media/Search/SearchBar.tsx @@ -1,109 +1,16 @@ -import React, { - FormEvent, - MouseEventHandler, - useContext, - useEffect, - useRef, - useState, -} from 'react'; -import {styled} from '@mui/material/styles'; -import {alpha, Box, Button, InputBase} from '@mui/material'; -import SearchIcon from '@mui/icons-material/Search'; +import {useContext} from 'react'; +import {Box} from '@mui/material'; import SearchFilters from './SearchFilters'; -import {useTranslation} from 'react-i18next'; import {SearchContext} from './SearchContext'; -import {ResultContext} from './ResultContext'; import SortBy from './Sorting/SortBy'; import {ZIndex} from '../../../themes/zIndex'; import GeoPointFilter from './GeoPointFilter'; -import AutoComplete from './AutoComplete'; -import {getSearchSuggestions, SearchSuggestion} from '../../../api/asset'; -import {GetSources} from '@algolia/autocomplete-core'; +import SearchAutoComplete from './SearchAutoComplete.tsx'; type Props = {}; -const Search = styled('div')(({theme}) => ({ - 'position': 'relative', - 'borderRadius': theme.shape.borderRadius, - 'backgroundColor': alpha(theme.palette.common.white, 0.35), - '&:hover': { - backgroundColor: alpha(theme.palette.common.white, 0.65), - }, - 'display': 'flex', - 'alignItems': 'center', - 'margin': theme.spacing(1), - [theme.breakpoints.up('sm')]: { - width: 'fit-content', - }, -})); - -const SearchIconWrapper = styled('div')(({theme}) => ({ - padding: theme.spacing(0, 2), - height: '100%', - position: 'absolute', - pointerEvents: 'none', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', -})); - -const StyledInputBase = styled(InputBase)(({theme}) => ({ - 'color': 'inherit', - '& .MuiInputBase-input': { - padding: theme.spacing(1, 1, 1, 0), - // vertical padding + font size from searchIcon - paddingLeft: `calc(1em + ${theme.spacing(4)})`, - width: '100%', - }, -})); - export default function SearchBar({}: Props) { const search = useContext(SearchContext); - const resultContext = useContext(ResultContext); - const [queryValue, setQueryValue] = useState(''); - const inputRef = useRef(null); - const {t} = useTranslation(); - - useEffect(() => { - setQueryValue(search.query); - }, [search.query]); - - const onClick: MouseEventHandler = () => { - if (search.query) { - setTimeout(() => { - if (inputRef.current?.value === '') { - search.setQuery('', true); - } - }, 10); - } - }; - - const onSubmit = (e: FormEvent) => { - e.preventDefault(); - search.setQuery(queryValue, true); - }; - - const getSources = React.useCallback>(() => { - return [ - { - sourceId: 'items', - onSelect: ({item, setQuery}) => { - const newQuery = `"${item.name}"`; - setQuery(newQuery); - setQueryValue(newQuery); - search.setQuery(newQuery, true); - }, - getItems({query}) { - return getSearchSuggestions(query).then(r => { - console.log('ES Debug', r.debug); - console.log('ES Query', JSON.stringify(r.debug.query)); - - return r.result; - }); - }, - }, - ]; - }, [search]); return ( - - {autocomplete => { - return ( -
) => { - autocomplete.setIsOpen(false); - onSubmit(e); - }} - > - - - - - - setQueryValue(e.target.value) - } - inputRef={inputRef} - onClick={onClick} - placeholder="Search…" - onKeyDown={e => e.stopPropagation()} // Prevent Ctrl + A propagation - onKeyPress={e => e.stopPropagation()} // Prevent Ctrl + A propagation - inputProps={{ - 'aria-label': 'search', - ...(autocomplete.getInputProps({ - inputElement: null, - onBlur: () => { - autocomplete.setIsOpen( - false - ); - }, - }) as any), - }} - /> - - -
- ); - }} -
+
diff --git a/databox/client/src/components/Media/TagFilterRule/FilterRule.tsx b/databox/client/src/components/Media/TagFilterRule/FilterRule.tsx index f9aebbc71..4853f6174 100644 --- a/databox/client/src/components/Media/TagFilterRule/FilterRule.tsx +++ b/databox/client/src/components/Media/TagFilterRule/FilterRule.tsx @@ -169,6 +169,7 @@ export default function FilterRule({ - - - + + + + + + ); } @@ -39,7 +43,11 @@ function WrapperComponent({children}: RouteWrapperProps) { <> - + {children} diff --git a/databox/client/src/components/Ui/Flag.tsx b/databox/client/src/components/Ui/Flag.tsx index 9a662f021..4f9ed5b33 100644 --- a/databox/client/src/components/Ui/Flag.tsx +++ b/databox/client/src/components/Ui/Flag.tsx @@ -34,6 +34,9 @@ function getLocaleCountryFlag(locale: string): FunctionComponent | undefined { } export default function Flag({locale, ...iconProps}: Props & IconProps) { + if (locale === 'en') { + locale = 'us'; + } const component = getLocaleCountryFlag(locale); if (component) { diff --git a/databox/client/src/components/Ui/Tabs.tsx b/databox/client/src/components/Ui/Tabs.tsx new file mode 100644 index 000000000..4fd4f5713 --- /dev/null +++ b/databox/client/src/components/Ui/Tabs.tsx @@ -0,0 +1,75 @@ +import {Tab, Tabs as BaseTabs} from '@mui/material'; +import React from 'react'; +import {TabItem} from '../Dialog/Tabbed/tabTypes.ts'; + +type Props

> = { + tabs: TabItem

[]; + currentTabId: string | undefined; + onTabChange: (tabId: string) => void; + onNoTab?: () => void; +} & P; + +export default function Tabs

>({ + tabs: configTabs, + onTabChange, + currentTabId, + onNoTab, + ...rest +}: Props

) { + const tabs = configTabs.filter(t => t.enabled ?? true); + const tabIndex = tabs.findIndex(t => t.id === currentTabId); + const currentTab = tabIndex >= 0 ? tabs[tabIndex] : undefined; + + React.useEffect(() => { + if (!currentTab && onNoTab) { + onNoTab(); + } + }, [currentTab, onNoTab]); + + const handleChange = (_event: React.SyntheticEvent, newValue: number) => { + if (tabs[newValue].component) { + onTabChange(tabs[newValue].id); + } + }; + + return ( + <> + + {tabs.map(t => { + return ( + { + e.preventDefault(); + e.stopPropagation(); + t.onClick!(); + } + : t.onClick + : undefined + } + /> + ); + })} + + {currentTab && currentTab.component + ? React.createElement(currentTab.component, { + ...rest, + ...currentTab.props, + }) + : ''} + + ); +} diff --git a/databox/client/src/components/Upload/SaveAsTemplateForm.tsx b/databox/client/src/components/Upload/SaveAsTemplateForm.tsx index 7d54e02cd..c2b966556 100644 --- a/databox/client/src/components/Upload/SaveAsTemplateForm.tsx +++ b/databox/client/src/components/Upload/SaveAsTemplateForm.tsx @@ -8,7 +8,7 @@ import { } from '@mui/material'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import {FormRow} from '@alchemy/react-form'; -import SwitchWidget from '../Form/SwitchWidget'; +import {SwitchWidget} from '@alchemy/react-form'; import {useAssetDataTemplateOptions} from '../Media/Asset/Attribute/useAssetDataTemplateOptions'; import {FormFieldErrors} from '@alchemy/react-form'; import React from 'react'; diff --git a/databox/client/src/components/Upload/UploadForm.tsx b/databox/client/src/components/Upload/UploadForm.tsx index aabd17413..4dfa265af 100644 --- a/databox/client/src/components/Upload/UploadForm.tsx +++ b/databox/client/src/components/Upload/UploadForm.tsx @@ -237,6 +237,7 @@ export const UploadForm: FC<{ {t('form.asset.tags.label', 'Tags')} (); @@ -70,7 +69,6 @@ export default function WorkflowView({modalIndex}: Props) { maxHeight: headerHeight, }, }} - modalIndex={modalIndex} fullScreen={true} title={ (null); diff --git a/databox/client/src/hooks/useSelectAllKey.ts b/databox/client/src/hooks/useSelectAllKey.ts new file mode 100644 index 000000000..1172ff511 --- /dev/null +++ b/databox/client/src/hooks/useSelectAllKey.ts @@ -0,0 +1,39 @@ +import React from 'react'; + +let listeners: ((e: KeyboardEvent) => void)[] = []; + +export function useSelectAllKey(handler: () => void, deps: any[]) { + React.useEffect(() => { + const f = (e: KeyboardEvent) => { + if (listeners[listeners.length - 1] !== f) { + return; + } + + if (e.ctrlKey && e.key === 'a') { + const activeElement = document.activeElement; + if ( + activeElement && + ['input', 'select', 'button', 'textarea'].includes( + activeElement.tagName.toLowerCase() + ) && + (activeElement as HTMLInputElement).type !== 'checkbox' + ) { + return; + } + + e.preventDefault(); + e.stopPropagation(); + handler(); + } + }; + + listeners.push(f); + + window.addEventListener('keydown', f); + + return () => { + listeners = listeners.filter(l => l !== f); + window.removeEventListener('keydown', f); + }; + }, deps); +} diff --git a/databox/client/src/routes.ts b/databox/client/src/routes.ts index ae577158a..146a99c60 100644 --- a/databox/client/src/routes.ts +++ b/databox/client/src/routes.ts @@ -8,6 +8,7 @@ import AppAuthorizationCodePage from './components/AppAuthorizationCodePage'; import {compileRoutes} from '@alchemy/navigation'; import BasketDialog from './components/Dialog/Basket/BasketDialog'; import BasketViewDialog from './components/Basket/BasketViewDialog'; +import AttributeEditorView from './components/AttributeEditor/AttributeEditorView.tsx'; const modalRoutes = { workspaces: { @@ -63,13 +64,17 @@ const modalRoutes = { component: WorkflowView, public: false, }, + attributesBatchEdit: { + path: '/attributes/editor', + component: AttributeEditorView, + public: false, + }, }; const routes = { app: { path: '/', component: App, - routes: modalRoutes, public: true, }, auth: { diff --git a/databox/client/src/types.ts b/databox/client/src/types.ts index 8e6f6308a..862283749 100644 --- a/databox/client/src/types.ts +++ b/databox/client/src/types.ts @@ -61,10 +61,12 @@ export interface Asset type AttrValue = any; +type AttributeOrigin = 'human' | 'machine' | 'fallback' | 'initial'; + export interface Attribute extends IPermissions { id: string; definition: AttributeDefinition; - origin: 'human' | 'machine'; + origin: AttributeOrigin; multiple: boolean; originVendor?: string; locale?: string | undefined; @@ -72,6 +74,7 @@ export interface Attribute extends IPermissions { originVendorContext?: string; value: AttrValue; highlight: AttrValue; + assetAnnotations?: AssetAnnotation[]; } export interface AssetFileVersion { @@ -87,6 +90,7 @@ export interface AttributeDefinition extends IPermissions { name: string; slug: string; fieldType: string; + entityType?: string | undefined; multiple: boolean; searchable: boolean; suggest: boolean; @@ -174,6 +178,20 @@ export interface TagFilterRule extends ApiHydraObjectResponse { exclude: Tag[]; } +type KeyTranslations = { + [locale: string]: string; +} + +export type AttributeEntity = { + id: string; + type: string; + locale: string; + value: string; + translations: KeyTranslations; + createdAt: string; + updatedAt: string; +} & ApiHydraObjectResponse; + export interface Tag extends ApiHydraObjectResponse, WithTranslations { id: string; name: string; @@ -221,18 +239,12 @@ export interface Basket extends IPermissions { export interface BasketAsset { id: string; asset: Asset; - context?: - | { - clip?: { - start?: number; - end?: number; - }; - } - | undefined; + context?: any; titleHighlight: string; position: number; createdAt: string; owner?: User; + assetAnnotations?: AssetAnnotation[]; } export interface Workspace extends IPermissions { @@ -302,3 +314,16 @@ export type StateSetter = (handler: T | ((prev: T) => T)) => void; export type AssetOrAssetContainer = { id: string; }; + +export enum AnnotationType { + Point = 'point', + Circle = 'circle', + Rect = 'rect', + Cue = 'cue', + TimeRange = 'time_range', +} + +export type AssetAnnotation = { + type: AnnotationType; + [prop: string]: any; +}; diff --git a/databox/client/src/utils/array.ts b/databox/client/src/utils/array.ts new file mode 100644 index 000000000..7e4135071 --- /dev/null +++ b/databox/client/src/utils/array.ts @@ -0,0 +1,7 @@ +export function pushUnique(array: T[], newValue: T): void { + if (array.some(i => i === newValue)) { + return; + } + + array.push(newValue); +} diff --git a/databox/client/src/utils/types.ts b/databox/client/src/utils/types.ts new file mode 100644 index 000000000..4a4634c2f --- /dev/null +++ b/databox/client/src/utils/types.ts @@ -0,0 +1 @@ +export type Optional = Pick, K> & Omit; diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index b0ff470a1..3ed0cd7a1 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -256,17 +256,6 @@ services: ports: - 127.0.0.1:${DB_DEV_PORT}:5432 - keycloak: - command: - - 'start-dev' - - '--hostname-strict-https=false' - - '--spi-connections-http-client-default-disable-trust-manager=true' - - keycloak2: - command: - - 'start-dev' - - '--hostname-strict-https=false' - cypress: entrypoint: "" command: npx cypress open --e2e --browser chrome --project ./ diff --git a/docker-compose.yml b/docker-compose.yml index 03c7adcb2..8af2d66d0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -920,14 +920,14 @@ services: target: keycloak command: - 'start' - - '--hostname-strict-https=false' + - '--optimized' networks: - internal environment: - KC_PROXY=edge - KEYCLOAK_ADMIN - KEYCLOAK_ADMIN_PASSWORD - - KC_HOSTNAME=keycloak.${PHRASEA_DOMAIN}${HTTPS_PORT_PREFIX} + - KC_HOSTNAME=https://keycloak.${PHRASEA_DOMAIN}${HTTPS_PORT_PREFIX} - KC_DB_USERNAME=${POSTGRES_USER} - KC_DB_PASSWORD=${POSTGRES_PASSWORD} - KC_DB=postgres @@ -941,10 +941,10 @@ services: - "traefik.http.services.keycloak.loadbalancer.server.port=8080" keycloak2: - image: quay.io/keycloak/keycloak:22.0.1 + image: ${REGISTRY_NAMESPACE}keycloak:${DOCKER_TAG} command: - 'start' - - '--hostname-strict-https=false' + - '--optimized' networks: - internal profiles: @@ -953,7 +953,7 @@ services: - KC_PROXY=edge - KEYCLOAK_ADMIN=${KEYCLOAK2_ADMIN} - KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK2_ADMIN_PASSWORD} - - KC_HOSTNAME=keycloak2.${PHRASEA_DOMAIN} + - KC_HOSTNAME=https://keycloak2.${PHRASEA_DOMAIN}${HTTPS_PORT_PREFIX} - KC_DB_USERNAME=${POSTGRES_USER} - KC_DB_PASSWORD=${POSTGRES_PASSWORD} - KC_DB=postgres @@ -1017,6 +1017,7 @@ services: - DEFAULT_ADMIN_PASSWORD - INDEXER_DATABOX_CLIENT_ID - INDEXER_DATABOX_CLIENT_SECRET + - PHRASEA_DOMAIN extra_hosts: - keycloak.${PHRASEA_DOMAIN}:${PS_GATEWAY_IP} volumes: diff --git a/infra/docker/keycloak/Dockerfile b/infra/docker/keycloak/Dockerfile index bb4c132c3..92e0cfff6 100644 --- a/infra/docker/keycloak/Dockerfile +++ b/infra/docker/keycloak/Dockerfile @@ -1,4 +1,4 @@ -FROM maven:3.9.3-amazoncorretto-8 as spi-builder +FROM maven:3.9.3-amazoncorretto-8 AS spi-builder WORKDIR /app @@ -12,18 +12,21 @@ COPY . . RUN mvn clean package -FROM quay.io/keycloak/keycloak:22.0.1 as builder +FROM quay.io/keycloak/keycloak:25.0.2 AS builder COPY --from=spi-builder /app/group-uuid-pmapper/target/group-uuid-pmapper.jar /opt/keycloak/providers/ COPY --from=spi-builder /app/jq-idp-mapper/target/jq-idp-mapper-jar-with-dependencies.jar /opt/keycloak/providers/ +ENV KC_HEALTH_ENABLED=true \ + KC_DB=postgres + RUN /opt/keycloak/bin/kc.sh build \ && mkdir /opt/keycloak/themes/phrasea COPY themes/phrasea /opt/keycloak/themes/phrasea -FROM quay.io/keycloak/keycloak:22.0.1 as keycloak +FROM quay.io/keycloak/keycloak:25.0.2 AS keycloak ENV KC_SPI_THEME_DEFAULT=phrasea diff --git a/infra/docker/keycloak/themes/phrasea/account/theme.properties b/infra/docker/keycloak/themes/phrasea/account/theme.properties index 2d72a6394..e57a5f7ef 100644 --- a/infra/docker/keycloak/themes/phrasea/account/theme.properties +++ b/infra/docker/keycloak/themes/phrasea/account/theme.properties @@ -1 +1 @@ -parent=keycloak.v2 +parent=keycloak.v3 diff --git a/lib/js/api/index.ts b/lib/js/api/index.ts index a44d0a54a..a211fcc0f 100644 --- a/lib/js/api/index.ts +++ b/lib/js/api/index.ts @@ -8,6 +8,7 @@ import { HttpClient, } from "./src/httpClient"; import useFormSubmit from "./src/useFormSubmit"; +import {getApiResponseError} from "./src/utils"; export { useCancelRequest, @@ -15,8 +16,8 @@ export { useRequestErrorHandler, createHttpClient, useFormSubmit, + getApiResponseError, }; - export * from './src/types'; export type { diff --git a/lib/js/api/src/utils.ts b/lib/js/api/src/utils.ts index 5d85e8842..bc82aea4a 100644 --- a/lib/js/api/src/utils.ts +++ b/lib/js/api/src/utils.ts @@ -1,2 +1,20 @@ export const hydraDescriptionKey = 'hydra:description'; + +export function getApiResponseError(e: any): string | undefined { + if (e.isAxiosError) { + const status = e.response?.status ?? 0; + const data = e.response.data; + if (status === 422 && data.violations) { + return data.violations.map((v: { + message: string; + }) => v.message).join("\n") + } + + if (data['hydra:description']) { + return `${data['hydra:title']}: ${data['hydra:description']}`; + } + + return data['hydra:title'] ?? data['error_message'] ?? data['error'] ?? 'Error'; + } +} diff --git a/lib/js/navigation/index.ts b/lib/js/navigation/index.ts index 7e80ab746..422844a32 100644 --- a/lib/js/navigation/index.ts +++ b/lib/js/navigation/index.ts @@ -10,7 +10,7 @@ import { useCloseOverlay, useNavigateToOverlay } from "./src/useNavigateToOverlay"; -import {Link, useLocation, useNavigate, useParams} from "react-router-dom"; +import {Link, useLocation, useNavigate, useParams, useNavigation} from "react-router-dom"; import OverlayOutlet from "./src/Overlay/OverlayOutlet"; import {useOverlay} from "./src/Overlay/OverlayContext"; @@ -35,6 +35,7 @@ export { useParams, Link, useNavigate, + useNavigation, } export type { StackedModalProps, diff --git a/lib/js/navigation/src/Overlay/OverlayOutlet.tsx b/lib/js/navigation/src/Overlay/OverlayOutlet.tsx index 8d8a1ee25..843b54814 100644 --- a/lib/js/navigation/src/Overlay/OverlayOutlet.tsx +++ b/lib/js/navigation/src/Overlay/OverlayOutlet.tsx @@ -2,7 +2,7 @@ import React, {PropsWithChildren, ReactNode, useRef} from 'react'; import {useLocation, useNavigate} from "react-router-dom"; import OverlayRouterProvider from "./OverlayRouterProvider"; import {getOverlayContext, TOverlayContext} from "./OverlayContext"; -import {Routes} from "../types"; +import {RouteProxyComponent, Routes} from "../types"; type OverlayComponentProps = PropsWithChildren<{ @@ -16,12 +16,14 @@ type Props = { queryParam: string; routes: Routes; WrapperComponent?: OverlayComponent; + RouteProxyComponent?: RouteProxyComponent; }; export default function OverlayOutlet({ queryParam, routes, WrapperComponent = DefaultWrapperComponent, + RouteProxyComponent, }: Props) { const location = useLocation(); const timer = useRef>(); @@ -83,6 +85,9 @@ export default function OverlayOutlet({ path={finalUrl} queryParam={queryParam} routes={routes} + options={{ + RouteProxyComponent, + }} /> : ''} diff --git a/lib/js/navigation/src/useNavigateToOverlay.tsx b/lib/js/navigation/src/useNavigateToOverlay.tsx index 2fd74a3d6..e06f88962 100644 --- a/lib/js/navigation/src/useNavigateToOverlay.tsx +++ b/lib/js/navigation/src/useNavigateToOverlay.tsx @@ -1,18 +1,18 @@ -import {useLocation, useNavigate} from "react-router-dom"; +import {useLocation, useNavigate, NavigateOptions} from "react-router-dom"; import React from "react"; import {RouteDefinition, RouteParameters} from "./types"; import {getPath} from "./Router"; -export type NavigateToOverlayFunction = (route: RouteDefinition, params?: RouteParameters) => void; -export type CloseOverlayFunction = () => void; +export type NavigateToOverlayFunction = (route: RouteDefinition, params?: RouteParameters, options?: NavigateOptions) => void; +export type CloseOverlayFunction = (options?: NavigateOptions) => void; export function useNavigateToOverlay(queryParam: string): NavigateToOverlayFunction { const navigate = useNavigate(); - return React.useCallback((route, params) => { + return React.useCallback((route, params, options) => { navigate({ search: `${queryParam}=${getPath(route, params)}`, - }); + }, options); }, []); } @@ -20,13 +20,13 @@ export function useCloseOverlay(queryParam: string): CloseOverlayFunction { const navigate = useNavigate(); const location = useLocation(); - return React.useCallback(() => { + return React.useCallback((options) => { const searchParams = new URLSearchParams(location.search); searchParams.delete(queryParam); navigate({ pathname: location.pathname, search: searchParams.toString(), - }); + }, options); }, [navigate, location]); } diff --git a/lib/js/phrasea-ui/src/components/Dialog/AppDialog.tsx b/lib/js/phrasea-ui/src/components/Dialog/AppDialog.tsx index d70b887df..178bf9629 100644 --- a/lib/js/phrasea-ui/src/components/Dialog/AppDialog.tsx +++ b/lib/js/phrasea-ui/src/components/Dialog/AppDialog.tsx @@ -115,6 +115,7 @@ export default function AppDialog({ dividers sx={{ p: disablePadding ? 0 : 2, + border: disablePadding ? 0 : undefined, }} > {children} diff --git a/lib/js/react-form/index.ts b/lib/js/react-form/index.ts index 0388fb677..78e23a203 100644 --- a/lib/js/react-form/index.ts +++ b/lib/js/react-form/index.ts @@ -9,8 +9,11 @@ import TranslationsWidget from "./src/Translations/TranslationsWidget"; import {ColorBox} from "./src/Color/ColorBox"; import TranslatedField from "./src/Translations/TranslatedField"; import LoadingButton from "./src/LoadingButton"; -import AsyncRSelectWidget, {AsyncRSelectProps} from "./src/AsyncRSelectWidget"; +import AsyncRSelectWidget, {AsyncRSelectProps, RSelectOnCreate} from "./src/AsyncRSelectWidget"; import RSelectWidget, {RSelectProps, SelectOption} from "./src/RSelectWidget"; +import SwitchWidget from "./src/Widget/SwitchWidget"; +import CheckboxWidget from "./src/Widget/CheckboxWidget"; +import KeyTranslationsWidget, {getNonEmptyTranslations} from "./src/Translations/KeyTranslationsWidget"; export { CollectionWidget, @@ -22,16 +25,21 @@ export { FormSection, SortableCollectionWidget, TranslationsWidget, + KeyTranslationsWidget, TranslatedField, LoadingButton, AsyncRSelectWidget, RSelectWidget, + SwitchWidget, + CheckboxWidget, + getNonEmptyTranslations, }; export type { AsyncRSelectProps, RSelectProps, SelectOption, + RSelectOnCreate, }; export type * from './src/types'; diff --git a/lib/js/react-form/src/AsyncRSelectWidget.tsx b/lib/js/react-form/src/AsyncRSelectWidget.tsx index 5dc2c2e80..275da388b 100644 --- a/lib/js/react-form/src/AsyncRSelectWidget.tsx +++ b/lib/js/react-form/src/AsyncRSelectWidget.tsx @@ -179,9 +179,7 @@ export default function AsyncRSelectWidget< ?.value; updateLastOptions([option]); - onChange(v); - onChangeProp && onChangeProp(newValue, { action: 'select-option', @@ -253,6 +251,7 @@ export default function AsyncRSelectWidget< const newValue = ( isMulti ? [option] : option ) as OnChangeValue; + updateLastOptions([option]); setValue(newValue); onChangeProp && onChangeProp(newValue, { diff --git a/lib/js/react-form/src/RSelectWidget.tsx b/lib/js/react-form/src/RSelectWidget.tsx index cd2d2e941..6e4177529 100644 --- a/lib/js/react-form/src/RSelectWidget.tsx +++ b/lib/js/react-form/src/RSelectWidget.tsx @@ -18,6 +18,7 @@ type Option = { label: string; value: string; image?: React.ElementType | React.FC; + item?: object | undefined; }; export type {Option as SelectOption}; diff --git a/lib/js/react-form/src/Translations/KeyTranslationsWidget.tsx b/lib/js/react-form/src/Translations/KeyTranslationsWidget.tsx new file mode 100644 index 000000000..5564019a7 --- /dev/null +++ b/lib/js/react-form/src/Translations/KeyTranslationsWidget.tsx @@ -0,0 +1,80 @@ +import {Stack, TextField} from '@mui/material'; +import {useTranslation} from 'react-i18next'; +import {TextFieldProps} from '@mui/material/TextField/TextField'; +import FormRow from "../FormRow"; +import FormFieldErrors from "../FormFieldErrors"; +import React, {ReactNode} from "react"; +import {FieldErrors, UseFormRegister} from "react-hook-form"; + +type KeyTranslations = { + [locale: string]: string; +} + +type Props = { + register: UseFormRegister; + inputProps?: TextFieldProps; + locales: string[]; + name: string; + errors: FieldErrors; + renderLocale: (locale: string) => ReactNode; +}; + +export default function KeyTranslationsWidget< + TFieldValues extends { translations: KeyTranslations }, +>({name, register, errors, inputProps, locales, renderLocale}: Props) { + const {t} = useTranslation(); + + const path = name; + + return ( + <> + {locales.map(l => { + return + + +

+ {renderLocale(l)} +
+
+ + +
+ + + + })} + + ); +} + +export function getNonEmptyTranslations(translations: KeyTranslations): KeyTranslations { + const tr: KeyTranslations = {}; + + Object.keys(translations).forEach(key => { + if (translations[key]) { + tr[key] = translations[key]; + } + }) + + return tr; +} diff --git a/databox/client/src/components/Form/CheckboxWidget.tsx b/lib/js/react-form/src/Widget/CheckboxWidget.tsx similarity index 100% rename from databox/client/src/components/Form/CheckboxWidget.tsx rename to lib/js/react-form/src/Widget/CheckboxWidget.tsx diff --git a/databox/client/src/components/Form/SwitchWidget.tsx b/lib/js/react-form/src/Widget/SwitchWidget.tsx similarity index 100% rename from databox/client/src/components/Form/SwitchWidget.tsx rename to lib/js/react-form/src/Widget/SwitchWidget.tsx diff --git a/lib/js/react-hooks/package.json b/lib/js/react-hooks/package.json index e452939ab..9ff1caebb 100644 --- a/lib/js/react-hooks/package.json +++ b/lib/js/react-hooks/package.json @@ -4,12 +4,17 @@ "public": true, "files": [ "src/createStateSetterProxy.ts", + "src/useDebounce.ts", "src/useDebounceLoader.ts", "src/useDebounceScroll.ts", "src/useEffectOnce.ts", - "src/useWindowSize.ts", + "src/useTimeout.ts", "src/useUniqueId.ts", - "src/useTimeout.ts" + "src/useWindowSize.ts", + "src/useUpdateEffect.ts", + "src/useMountEffect.ts", + "src/useElementResize.ts", + "src/deep.ts" ], "peerDependencies": { "axios": "^1.6.2", diff --git a/lib/js/react-hooks/src/deep.ts b/lib/js/react-hooks/src/deep.ts new file mode 100644 index 000000000..e3de48fc8 --- /dev/null +++ b/lib/js/react-hooks/src/deep.ts @@ -0,0 +1,27 @@ +export function isObject(item: any): boolean { + return (item && typeof item === 'object' && !Array.isArray(item)); +} + +export function deepMerge(target: any, ...sources: Array) { + if (!sources.length) { + return target; + } + + const source = sources.shift(); + + if (isObject(target) && isObject(source)) { + for (const key in source) { + if (isObject(source[key])) { + if (!target[key]) { + Object.assign(target, {[key]: {}}); + } + + deepMerge(target[key], source[key]); + } else { + Object.assign(target, {[key]: source[key]}); + } + } + } + + return deepMerge(target, ...sources); +} diff --git a/lib/js/react-hooks/src/useDebounce.ts b/lib/js/react-hooks/src/useDebounce.ts new file mode 100644 index 000000000..b30c80324 --- /dev/null +++ b/lib/js/react-hooks/src/useDebounce.ts @@ -0,0 +1,14 @@ +import {useRef} from 'react'; + +export function useDebounce() { + const timer = useRef>(); + + return (handler: () => any, delay?: number) => { + clearTimeout(timer.current); + + timer.current = setTimeout( + handler, + delay, + ); + } +} diff --git a/lib/js/react-hooks/src/useEffectOnce.ts b/lib/js/react-hooks/src/useEffectOnce.ts index 38ba81978..eaa828002 100644 --- a/lib/js/react-hooks/src/useEffectOnce.ts +++ b/lib/js/react-hooks/src/useEffectOnce.ts @@ -1,17 +1,9 @@ import {useEffect, useRef} from "react"; +import {propsAreSame} from "./utils"; -function propsAreSame(a: any[], b: any[]): boolean { - for (const i in a) { - if (b[i] !== a[i]) { - return false; - } - } - - return true; -} export default function useEffectOnce( - handler: () => void, + handler: () => any, trackingValues: any[], ) { const runRef = useRef(false); diff --git a/lib/js/react-hooks/src/useElementResize.ts b/lib/js/react-hooks/src/useElementResize.ts new file mode 100644 index 000000000..e86226636 --- /dev/null +++ b/lib/js/react-hooks/src/useElementResize.ts @@ -0,0 +1,37 @@ +import React, {useRef} from 'react'; + +export function useElementResize(element: HTMLElement | null | undefined) { + const timer = useRef>(); + const [size, setSize] = React.useState<{ + width: number; + height: number; + }>(); + + React.useEffect(() => { + if (!element) { + return; + } + + const resizeObserver = new ResizeObserver((entries) => { + const entry = (entries[0])?.target as HTMLElement | undefined; + if (entry) { + if (timer.current) { + clearTimeout(timer.current); + } + timer.current = setTimeout( + () => { + setSize({ + width: entry.clientWidth, + height: entry.clientHeight, + }) + }, + 100, + ); + } + }); + + resizeObserver.observe(element); + }, [element, timer]); + + return size; +} diff --git a/lib/js/react-hooks/src/useMountEffect.ts b/lib/js/react-hooks/src/useMountEffect.ts new file mode 100644 index 000000000..c8007d027 --- /dev/null +++ b/lib/js/react-hooks/src/useMountEffect.ts @@ -0,0 +1,12 @@ +import {useEffect, useRef} from 'react'; +export default function useMountEffect(effect: () => any, dependencies: any[] = []) { + const isInitialMount = useRef(true); + + useEffect(() => { + if (isInitialMount.current) { + isInitialMount.current = false; + + return effect(); + } + }, dependencies); +} diff --git a/lib/js/react-hooks/src/useUpdateEffect.ts b/lib/js/react-hooks/src/useUpdateEffect.ts new file mode 100644 index 000000000..bc46e45ed --- /dev/null +++ b/lib/js/react-hooks/src/useUpdateEffect.ts @@ -0,0 +1,13 @@ +import {useEffect, useRef} from 'react'; +import {propsAreSame} from "./utils"; +export default function useUpdateEffect(effect: () => any, trackingValues: any[] = []) { + const trackingRef = useRef(trackingValues); + + useEffect(() => { + if (!propsAreSame(trackingRef.current, trackingValues)) { + trackingRef.current = trackingValues; + + return effect(); + } + }, trackingValues); +} diff --git a/lib/js/react-hooks/src/utils.ts b/lib/js/react-hooks/src/utils.ts new file mode 100644 index 000000000..8763415f6 --- /dev/null +++ b/lib/js/react-hooks/src/utils.ts @@ -0,0 +1,10 @@ + +export function propsAreSame(a: any[], b: any[]): boolean { + for (const i in a) { + if (b[i] !== a[i]) { + return false; + } + } + + return true; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cdd699c1d..265beb8ae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,10 +37,10 @@ importers: version: link:../../lib/js/theme-editor '@mui/icons-material': specifier: ^5.15.13 - version: 5.15.13(@mui/material@5.15.13)(@types/react@18.2.65)(react@18.2.0) + version: 5.15.13(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.2.65)(react@18.2.0) '@mui/material': specifier: ^5.15.13 - version: 5.15.13(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + version: 5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: specifier: ^18.2.0 version: 18.2.0 @@ -49,13 +49,13 @@ importers: version: 18.2.0(react@18.2.0) react-google-font-loader: specifier: ^1.1.0 - version: 1.1.0(react-dom@18.2.0)(react@18.2.0) + version: 1.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-i18next: specifier: ^14.1.0 - version: 14.1.0(i18next@23.11.4)(react-dom@18.2.0)(react@18.2.0) + version: 14.1.0(i18next@23.11.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) vite-plugin-svgr: specifier: ^4.2.0 - version: 4.2.0(typescript@5.4.2)(vite@5.1.6) + version: 4.2.0(rollup@4.13.0)(typescript@5.4.2)(vite@5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3)) devDependencies: '@types/node': specifier: ^18.19.24 @@ -68,13 +68,13 @@ importers: version: 18.2.22 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.4.2) + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint@8.57.0)(typescript@5.4.2) '@typescript-eslint/parser': specifier: ^6.21.0 version: 6.21.0(eslint@8.57.0)(typescript@5.4.2) '@vitejs/plugin-react-swc': specifier: ^3.6.0 - version: 3.6.0(vite@5.1.6) + version: 3.6.0(vite@5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3)) eslint: specifier: ^8.57.0 version: 8.57.0 @@ -83,10 +83,10 @@ importers: version: 5.4.2 vite: specifier: ^5.1.6 - version: 5.1.6(@types/node@18.19.24)(sass@1.71.1) + version: 5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3) vite-plugin-checker: specifier: ^0.6.4 - version: 0.6.4(eslint@8.57.0)(typescript@5.4.2)(vite@5.1.6) + version: 0.6.4(eslint@8.57.0)(optionator@0.9.3)(typescript@5.4.2)(vite@5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3))(vue-tsc@1.8.27(typescript@5.4.2)) databox/client: dependencies: @@ -134,10 +134,10 @@ importers: version: 1.17.0 '@dnd-kit/core': specifier: ^6.1.0 - version: 6.1.0(react-dom@18.2.0)(react@18.2.0) + version: 6.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@dnd-kit/sortable': specifier: ^7.0.2 - version: 7.0.2(@dnd-kit/core@6.1.0)(react@18.2.0) + version: 7.0.2(@dnd-kit/core@6.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0) '@dnd-kit/utilities': specifier: ^3.2.2 version: 3.2.2(react@18.2.0) @@ -146,22 +146,22 @@ importers: version: 11.11.4(@types/react@18.2.65)(react@18.2.0) '@emotion/styled': specifier: ^11.11.0 - version: 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.65)(react@18.2.0) + version: 11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0) '@mui/icons-material': specifier: ^5.15.13 - version: 5.15.13(@mui/material@5.15.13)(@types/react@18.2.65)(react@18.2.0) + version: 5.15.13(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.2.65)(react@18.2.0) '@mui/lab': specifier: 5.0.0-alpha.168 - version: 5.0.0-alpha.168(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@mui/material@5.15.13)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + version: 5.0.0-alpha.168(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@mui/material': specifier: ^5.15.13 - version: 5.15.13(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + version: 5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@mui/x-tree-view': specifier: ^6.17.0 - version: 6.17.0(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@mui/material@5.15.13)(@mui/system@5.15.15)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + version: 6.17.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mui/system@5.15.15(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@toast-ui/react-image-editor': specifier: ^3.15.2 - version: 3.15.2(react@18.2.0) + version: 3.15.2(encoding@0.1.13)(react@18.2.0) ace-builds: specifier: ^1.32.7 version: 1.32.7 @@ -188,7 +188,7 @@ importers: version: 2.4.5(react@18.2.0) formik-material-ui: specifier: 4.0.0-alpha.2 - version: 4.0.0-alpha.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@mui/material@5.15.13)(formik@2.4.5)(react@18.2.0)(tiny-warning@1.0.3) + version: 4.0.0-alpha.2(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(formik@2.4.5(react@18.2.0))(react@18.2.0)(tiny-warning@1.0.3) ismounted: specifier: ^0.1.8 version: 0.1.8(react@18.2.0) @@ -201,21 +201,24 @@ importers: pusher-js: specifier: ^8.3.0 version: 8.3.0 + re-resizable: + specifier: ^6.9.17 + version: 6.9.17(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: specifier: ^18.2.0 version: 18.2.0 react-ace: specifier: ^10.1.0 - version: 10.1.0(react-dom@18.2.0)(react@18.2.0) + version: 10.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-colorful: specifier: ^5.6.1 - version: 5.6.1(react-dom@18.2.0)(react@18.2.0) + version: 5.6.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-compare-image: specifier: ^3.4.0 - version: 3.4.0(react-dom@18.2.0)(react@18.2.0) + version: 3.4.0(canvas@2.11.2(encoding@0.1.13))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-dnd: specifier: ^16.0.1 - version: 16.0.1(@types/node@18.19.24)(@types/react@18.2.65)(react@18.2.0) + version: 16.0.1(@types/hoist-non-react-statics@3.3.5)(@types/node@18.19.24)(@types/react@18.2.65)(react@18.2.0) react-dnd-html5-backend: specifier: ^16.0.1 version: 16.0.1 @@ -230,37 +233,37 @@ importers: version: 7.51.0(react@18.2.0) react-i18next: specifier: ^14.1.0 - version: 14.1.0(i18next@23.11.4)(react-dom@18.2.0)(react@18.2.0) + version: 14.1.0(i18next@23.11.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-leaflet: specifier: ^4.2.1 - version: 4.2.1(leaflet@1.9.4)(react-dom@18.2.0)(react@18.2.0) + version: 4.2.1(leaflet@1.9.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-nl2br: specifier: ^1.0.4 version: 1.0.4(react@18.2.0) react-pdf: specifier: ^7.7.1 - version: 7.7.1(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + version: 7.7.1(@types/react@18.2.65)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-player: specifier: ^2.15.1 version: 2.15.1(react@18.2.0) react-select: specifier: ^5.8.0 - version: 5.8.0(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + version: 5.8.0(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-string-replace: specifier: ^1.1.1 version: 1.1.1 react-toastify: specifier: ^9.1.3 - version: 9.1.3(react-dom@18.2.0)(react@18.2.0) + version: 9.1.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-virtualized: specifier: ^9.22.5 - version: 9.22.5(react-dom@18.2.0)(react@18.2.0) + version: 9.22.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) sass: specifier: ^1.71.1 version: 1.71.1 tui-image-editor: specifier: ^3.15.3 - version: 3.15.3 + version: 3.15.3(encoding@0.1.13) uuid: specifier: ^9.0.1 version: 9.0.1 @@ -269,14 +272,14 @@ importers: version: 2.1.4 zustand: specifier: ^4.5.2 - version: 4.5.2(@types/react@18.2.65)(react@18.2.0) + version: 4.5.2(@types/react@18.2.65)(immer@10.0.4)(react@18.2.0) devDependencies: '@testing-library/jest-dom': specifier: ^6.4.2 - version: 6.4.2(@types/jest@27.5.2)(jest@23.6.0) + version: 6.4.2(@types/jest@27.5.2) '@testing-library/react': specifier: ^14.2.1 - version: 14.2.1(react-dom@18.2.0)(react@18.2.0) + version: 14.2.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@testing-library/user-event': specifier: ^14.5.2 version: 14.5.2(@testing-library/dom@10.0.0) @@ -300,25 +303,25 @@ importers: version: 18.2.22 '@types/react-select': specifier: ^5.0.1 - version: 5.0.1(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + version: 5.0.1(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@types/react-virtualized': specifier: ^9.21.29 version: 9.21.29 '@types/testing-library__jest-dom': specifier: ^6.0.0 - version: 6.0.0(@types/jest@27.5.2)(jest@23.6.0) + version: 6.0.0(@types/jest@27.5.2) '@types/uuid': specifier: ^9.0.8 version: 9.0.8 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.4.2) + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint@8.57.0)(typescript@5.4.2) '@typescript-eslint/parser': specifier: ^6.21.0 version: 6.21.0(eslint@8.57.0)(typescript@5.4.2) '@vitejs/plugin-react-swc': specifier: ^3.6.0 - version: 3.6.0(vite@5.1.6) + version: 3.6.0(vite@5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3)) esbuild-plugin-react-virtualized: specifier: ^1.0.4 version: 1.0.4(esbuild@0.20.2) @@ -330,7 +333,7 @@ importers: version: 0.4.6(eslint@8.57.0) eslint-plugin-unused-imports: specifier: ^3.1.0 - version: 3.1.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint@8.57.0) + version: 3.1.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint@8.57.0)(typescript@5.4.2))(eslint@8.57.0) i18next-scanner: specifier: ^4.4.0 version: 4.4.0 @@ -345,13 +348,13 @@ importers: version: 5.4.2 vite: specifier: ^5.1.6 - version: 5.1.6(@types/node@18.19.24)(sass@1.71.1) + version: 5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3) vite-plugin-checker: specifier: ^0.6.4 - version: 0.6.4(eslint@8.57.0)(typescript@5.4.2)(vite@5.1.6) + version: 0.6.4(eslint@8.57.0)(optionator@0.9.3)(typescript@5.4.2)(vite@5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3))(vue-tsc@1.8.27(typescript@5.4.2)) vite-plugin-svgr: specifier: ^4.2.0 - version: 4.2.0(typescript@5.4.2)(vite@5.1.6) + version: 4.2.0(rollup@4.13.0)(typescript@5.4.2)(vite@5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3)) databox/indexer: dependencies: @@ -394,7 +397,7 @@ importers: devDependencies: '@api-platform/create-client': specifier: ^0.10.0 - version: 0.10.0 + version: 0.10.0(encoding@0.1.13)(web-streams-polyfill@3.3.3) '@types/amqplib': specifier: ^0.10.5 version: 0.10.5 @@ -412,7 +415,7 @@ importers: version: 1.12.16 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.4.2) + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint@8.57.0)(typescript@5.4.2) '@typescript-eslint/parser': specifier: ^6.21.0 version: 6.21.0(eslint@8.57.0)(typescript@5.4.2) @@ -424,16 +427,16 @@ importers: version: 5.0.5 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@18.19.24)(typescript@5.4.2) + version: 10.9.2(@swc/core@1.4.7)(@types/node@18.19.24)(typescript@5.4.2) typescript: specifier: ^5.4.2 version: 5.4.2 vite: specifier: ^5.1.6 - version: 5.1.6(@types/node@18.19.24)(sass@1.71.1) + version: 5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3) vite-plugin-node: specifier: ^3.1.0 - version: 3.1.0(vite@5.1.6) + version: 3.1.0(@swc/core@1.4.7)(vite@5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3)) expose/client: dependencies: @@ -469,10 +472,10 @@ importers: version: 5.5.1(react@18.2.0) '@mui/icons-material': specifier: ^5.15.13 - version: 5.15.13(@mui/material@5.15.13)(@types/react@18.2.65)(react@18.2.0) + version: 5.15.13(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.2.65)(react@18.2.0) '@mui/material': specifier: ^5.15.13 - version: 5.15.13(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + version: 5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) axios: specifier: ^1.6.7 version: 1.6.7 @@ -493,28 +496,28 @@ importers: version: 18.2.0 react-bootstrap: specifier: ^1.6.8 - version: 1.6.8(react-dom@18.2.0)(react@18.2.0) + version: 1.6.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) react-grid-gallery: specifier: ^0.5.6 - version: 0.5.6(react-dom@18.2.0)(react@18.2.0) + version: 0.5.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-i18next: specifier: ^14.1.0 - version: 14.1.0(i18next@23.11.4)(react-dom@18.2.0)(react@18.2.0) + version: 14.1.0(i18next@23.11.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-image-gallery: specifier: ^0.9.1 version: 0.9.1(react@18.2.0) react-image-magnifiers: specifier: ^1.4.0 - version: 1.4.0(react-dom@18.2.0)(react@18.2.0) + version: 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-images: specifier: ^1.1.7 - version: 1.1.7(@types/react@18.2.65)(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0) + version: 1.1.7(@types/react@18.2.65)(prop-types@15.8.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-toastify: specifier: ^9.1.3 - version: 9.1.3(react-dom@18.2.0)(react@18.2.0) + version: 9.1.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) sass: specifier: ^1.71.1 version: 1.71.1 @@ -551,13 +554,13 @@ importers: version: 7.3.57 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.4.2) + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint@8.57.0)(typescript@5.4.2) '@typescript-eslint/parser': specifier: ^6.21.0 version: 6.21.0(eslint@8.57.0)(typescript@5.4.2) '@vitejs/plugin-react-swc': specifier: ^3.6.0 - version: 3.6.0(vite@5.1.6) + version: 3.6.0(vite@5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3)) eslint: specifier: ^8.57.0 version: 8.57.0 @@ -578,13 +581,13 @@ importers: version: 5.4.2 vite: specifier: ^5.1.6 - version: 5.1.6(@types/node@18.19.24)(sass@1.71.1) + version: 5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3) vite-plugin-checker: specifier: ^0.6.4 - version: 0.6.4(eslint@8.57.0)(typescript@5.4.2)(vite@5.1.6) + version: 0.6.4(eslint@8.57.0)(optionator@0.9.3)(typescript@5.4.2)(vite@5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3))(vue-tsc@1.8.27(typescript@5.4.2)) vite-plugin-svgr: specifier: ^4.2.0 - version: 4.2.0(typescript@5.4.2)(vite@5.1.6) + version: 4.2.0(rollup@4.13.0)(typescript@5.4.2)(vite@5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3)) lib/js/api: devDependencies: @@ -608,10 +611,10 @@ importers: version: 7.51.0(react@18.2.0) react-i18next: specifier: ^14.1.1 - version: 14.1.1(i18next@23.11.4)(react-dom@18.2.0)(react@18.2.0) + version: 14.1.1(i18next@23.11.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-toastify: specifier: ^9.1.3 - version: 9.1.3(react-dom@18.2.0)(react@18.2.0) + version: 9.1.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) lib/js/auth: dependencies: @@ -645,7 +648,7 @@ importers: version: 1.6.7 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@10.17.60)(typescript@5.4.2) + version: 10.9.2(@swc/core@1.4.7)(@types/node@10.17.60)(typescript@5.4.2) typescript: specifier: ^5.4.2 version: 5.4.2 @@ -679,14 +682,14 @@ importers: version: 7.2.1 i18next-http-backend: specifier: ^2.5.2 - version: 2.5.2 + version: 2.5.2(encoding@0.1.13) i18next-localstorage-backend: specifier: ^4.2.0 version: 4.2.0 devDependencies: react-i18next: specifier: ^14.1.1 - version: 14.1.1(i18next@23.11.4)(react-dom@18.2.0)(react@18.2.0) + version: 14.1.1(i18next@23.11.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) lib/js/liform-react: dependencies: @@ -713,7 +716,7 @@ importers: version: 4.2.1 redux-form: specifier: ^8.3.10 - version: 8.3.10(react-redux@9.1.0)(react@18.2.0)(redux@4.2.1) + version: 8.3.10(immutable@4.3.5)(react-redux@9.1.0(@types/react@18.2.65)(react@18.2.0)(redux@4.2.1))(react@18.2.0)(redux@4.2.1) devDependencies: react: specifier: ^18.2.0 @@ -732,7 +735,7 @@ importers: version: 1.11.0 react-router-dom: specifier: 6.18.0 - version: 6.18.0(react-dom@18.2.0)(react@18.2.0) + version: 6.18.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) devDependencies: '@alchemy/phrasea-ui': specifier: workspace:* @@ -757,20 +760,20 @@ importers: version: 18.2.0 react-i18next: specifier: ^14.1.1 - version: 14.1.1(i18next@23.11.4)(react-dom@18.2.0)(react@18.2.0) + version: 14.1.1(i18next@23.11.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) lib/js/phrasea-ui: dependencies: styled-components: specifier: ^6.1.8 - version: 6.1.8(react-dom@18.2.0)(react@18.2.0) + version: 6.1.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0) devDependencies: '@mui/icons-material': specifier: ^5.15.13 - version: 5.15.13(@mui/material@5.15.13)(@types/react@18.2.65)(react@18.2.0) + version: 5.15.13(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.2.65)(react@18.2.0) '@mui/material': specifier: ^5.15.13 - version: 5.15.13(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + version: 5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@types/react': specifier: ^18.2.65 version: 18.2.65 @@ -785,7 +788,7 @@ importers: version: 18.2.0 react-i18next: specifier: ^14.1.1 - version: 14.1.1(i18next@23.11.4)(react-dom@18.2.0)(react@18.2.0) + version: 14.1.1(i18next@23.11.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) lib/js/react-auth: dependencies: @@ -816,10 +819,10 @@ importers: version: 0.7.0(react@18.2.0) '@mui/lab': specifier: 5.0.0-alpha.168 - version: 5.0.0-alpha.168(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@mui/material@5.15.13)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + version: 5.0.0-alpha.168(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@mui/material': specifier: ^5.15.13 - version: 5.15.13(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + version: 5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@types/jest': specifier: ^23.3.14 version: 23.3.14 @@ -843,16 +846,16 @@ importers: version: 1.2.0 react-i18next: specifier: ^14.1.1 - version: 14.1.1(i18next@23.11.4)(react-dom@18.2.0)(react@18.2.0) + version: 14.1.1(i18next@23.11.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-router-dom: specifier: 6.18.0 - version: 6.18.0(react-dom@18.2.0)(react@18.2.0) + version: 6.18.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-toastify: specifier: ^9.1.3 - version: 9.1.3(react-dom@18.2.0)(react@18.2.0) + version: 9.1.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@10.17.60)(typescript@5.4.2) + version: 10.9.2(@swc/core@1.4.7)(@types/node@10.17.60)(typescript@5.4.2) typescript: specifier: ^5.4.2 version: 5.4.2 @@ -861,37 +864,37 @@ importers: dependencies: '@dnd-kit/core': specifier: ^6.0.5 - version: 6.1.0(react-dom@18.2.0)(react@18.2.0) + version: 6.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@dnd-kit/sortable': specifier: ^7.0.1 - version: 7.0.2(@dnd-kit/core@6.1.0)(react@18.2.0) + version: 7.0.2(@dnd-kit/core@6.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0) '@dnd-kit/utilities': specifier: ^3.2.0 version: 3.2.2(react@18.2.0) '@mui/icons-material': specifier: ^5.15.12 - version: 5.15.13(@mui/material@5.15.13)(@types/react@18.2.65)(react@18.2.0) + version: 5.15.13(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.2.65)(react@18.2.0) '@mui/lab': specifier: ^5.0.0-alpha.167 - version: 5.0.0-alpha.168(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@mui/material@5.15.13)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + version: 5.0.0-alpha.168(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: specifier: ^18.2.0 version: 18.2.0 react-colorful: specifier: ^5.6.1 - version: 5.6.1(react-dom@18.2.0)(react@18.2.0) + version: 5.6.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-hook-form: specifier: ^7.51.0 version: 7.51.0(react@18.2.0) react-i18next: specifier: ^14.1.1 - version: 14.1.1(i18next@23.11.4)(react-dom@18.2.0)(react@18.2.0) + version: 14.1.1(i18next@23.11.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-nl2br: specifier: ^1.0.4 version: 1.0.4(react@18.2.0) react-select: specifier: ^5.8.0 - version: 5.8.0(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + version: 5.8.0(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) devDependencies: '@alchemy/api': specifier: workspace:* @@ -907,7 +910,7 @@ importers: version: link:../phrasea-ui '@mui/material': specifier: ^5.15.13 - version: 5.15.13(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + version: 5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@types/node': specifier: ^10.17.60 version: 10.17.60 @@ -922,7 +925,7 @@ importers: version: 1.2.0 react-toastify: specifier: ^9.1.3 - version: 9.1.3(react-dom@18.2.0)(react@18.2.0) + version: 9.1.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) typescript: specifier: ^5.4.2 version: 5.4.2 @@ -1025,7 +1028,7 @@ importers: version: 18.2.0(react@18.2.0) react-i18next: specifier: ^14.1.1 - version: 14.1.1(i18next@23.11.4)(react-dom@18.2.0)(react@18.2.0) + version: 14.1.1(i18next@23.11.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) replace-in-file: specifier: ^3.4.4 version: 3.4.4 @@ -1052,7 +1055,7 @@ importers: version: 0.18.1(rollup@0.67.4)(typescript@5.4.2) semantic-release: specifier: ^15.14.0 - version: 15.14.0(@octokit/core@6.1.2) + version: 15.14.0(@octokit/core@6.1.2)(encoding@0.1.13) shelljs: specifier: ^0.8.5 version: 0.8.5 @@ -1073,7 +1076,7 @@ importers: version: 1.18.0 tslint-config-standard: specifier: ^8.0.1 - version: 8.0.1(tslint@5.20.1)(typescript@5.4.2) + version: 8.0.1(tslint@5.20.1(typescript@5.4.2))(typescript@5.4.2) typedoc: specifier: ^0.12.0 version: 0.12.0 @@ -1101,10 +1104,10 @@ importers: version: 11.11.4(@types/react@18.2.65)(react@18.2.0) '@emotion/styled': specifier: ^11.11.0 - version: 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.65)(react@18.2.0) + version: 11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0) '@mui/material': specifier: ^5.15.13 - version: 5.15.13(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + version: 5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@types/react': specifier: ^18.2.65 version: 18.2.65 @@ -1122,20 +1125,20 @@ importers: dependencies: reactflow: specifier: ^11.10.4 - version: 11.10.4(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + version: 11.10.4(@types/react@18.2.65)(immer@10.0.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) devDependencies: '@mui/icons-material': specifier: ^5.15.13 - version: 5.15.13(@mui/material@5.15.13)(@types/react@18.2.65)(react@18.2.0) + version: 5.15.13(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.2.65)(react@18.2.0) '@mui/lab': specifier: 5.0.0-alpha.168 - version: 5.0.0-alpha.168(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@mui/material@5.15.13)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + version: 5.0.0-alpha.168(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@mui/material': specifier: ^5.15.13 - version: 5.15.13(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + version: 5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@storybook/addon-essentials': specifier: ^7.6.17 - version: 7.6.17(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + version: 7.6.17(@types/react-dom@18.2.22)(@types/react@18.2.65)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@storybook/addon-interactions': specifier: ^7.6.17 version: 7.6.17 @@ -1144,13 +1147,13 @@ importers: version: 7.6.17(react@18.2.0) '@storybook/blocks': specifier: ^7.6.17 - version: 7.6.17(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + version: 7.6.17(@types/react-dom@18.2.22)(@types/react@18.2.65)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@storybook/react': specifier: ^7.6.17 - version: 7.6.17(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.2) + version: 7.6.17(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.4.2) '@storybook/react-vite': specifier: ^7.6.17 - version: 7.6.17(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.2)(vite@5.1.6) + version: 7.6.17(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(rollup@4.13.0)(typescript@5.4.2)(vite@5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3)) '@storybook/testing-library': specifier: 0.0.14-next.2 version: 0.0.14-next.2 @@ -1165,7 +1168,7 @@ importers: version: 18.2.22 '@vitejs/plugin-react': specifier: ^3.1.0 - version: 3.1.0(vite@5.1.6) + version: 3.1.0(vite@5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3)) moment: specifier: ^2.30.1 version: 2.30.1 @@ -1183,16 +1186,16 @@ importers: version: 1.71.1 storybook: specifier: ^7.6.17 - version: 7.6.17 + version: 7.6.17(encoding@0.1.13) typescript: specifier: ^5.4.2 version: 5.4.2 vite: specifier: ^5.1.6 - version: 5.1.6(@types/node@18.19.24)(sass@1.71.1) + version: 5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3) vite-plugin-dts: specifier: ^3.7.3 - version: 3.7.3(@types/node@18.19.24)(typescript@5.4.2)(vite@5.1.6) + version: 3.7.3(@types/node@18.19.24)(rollup@4.13.0)(typescript@5.4.2)(vite@5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3)) uploader/client: dependencies: @@ -1225,13 +1228,13 @@ importers: version: link:../../lib/js/react-ps '@mui/icons-material': specifier: ^5.15.13 - version: 5.15.13(@mui/material@5.15.13)(@types/react@18.2.65)(react@18.2.0) + version: 5.15.13(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.2.65)(react@18.2.0) '@mui/material': specifier: ^5.15.13 - version: 5.15.13(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + version: 5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@reduxjs/toolkit': specifier: ^2.2.1 - version: 2.2.1(react-redux@9.1.0)(react@18.2.0) + version: 2.2.1(react-redux@9.1.0(@types/react@18.2.65)(react@18.2.0)(redux@4.2.1))(react@18.2.0) axios: specifier: ^1.6.7 version: 1.6.7 @@ -1246,10 +1249,10 @@ importers: version: 18.2.0 react-bootstrap: specifier: ^1.6.8 - version: 1.6.8(react-dom@18.2.0)(react@18.2.0) + version: 1.6.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-burger-menu: specifier: ^2.9.2 - version: 2.9.2(react-dom@18.2.0)(react@18.2.0) + version: 2.9.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) @@ -1258,25 +1261,25 @@ importers: version: 14.2.3(react@18.2.0) react-i18next: specifier: ^14.1.0 - version: 14.1.0(i18next@23.11.4)(react-dom@18.2.0)(react@18.2.0) + version: 14.1.0(i18next@23.11.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-redux: specifier: ^9.1.0 version: 9.1.0(@types/react@18.2.65)(react@18.2.0)(redux@4.2.1) react-toastify: specifier: ^9.1.3 - version: 9.1.3(react-dom@18.2.0)(react@18.2.0) + version: 9.1.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) redux: specifier: ^4.2.1 version: 4.2.1 redux-form: specifier: ^8.3.10 - version: 8.3.10(react-redux@9.1.0)(react@18.2.0)(redux@4.2.1) + version: 8.3.10(immutable@4.3.5)(react-redux@9.1.0(@types/react@18.2.65)(react@18.2.0)(redux@4.2.1))(react@18.2.0)(redux@4.2.1) url: specifier: ^0.11.3 version: 0.11.3 vite-plugin-svgr: specifier: ^4.2.0 - version: 4.2.0(typescript@5.4.2)(vite@5.1.6) + version: 4.2.0(rollup@4.13.0)(typescript@5.4.2)(vite@5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3)) devDependencies: '@types/node': specifier: ^18.19.24 @@ -1292,13 +1295,13 @@ importers: version: 18.2.22 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.4.2) + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint@8.57.0)(typescript@5.4.2) '@typescript-eslint/parser': specifier: ^6.21.0 version: 6.21.0(eslint@8.57.0)(typescript@5.4.2) '@vitejs/plugin-react-swc': specifier: ^3.6.0 - version: 3.6.0(vite@5.1.6) + version: 3.6.0(vite@5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3)) eslint: specifier: ^8.57.0 version: 8.57.0 @@ -1319,10 +1322,10 @@ importers: version: 5.4.2 vite: specifier: ^5.1.6 - version: 5.1.6(@types/node@18.19.24)(sass@1.71.1) + version: 5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3) vite-plugin-checker: specifier: ^0.6.4 - version: 0.6.4(eslint@8.57.0)(typescript@5.4.2)(vite@5.1.6) + version: 0.6.4(eslint@8.57.0)(optionator@0.9.3)(typescript@5.4.2)(vite@5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3))(vue-tsc@1.8.27(typescript@5.4.2)) packages: @@ -2691,6 +2694,7 @@ packages: '@humanwhocodes/config-array@0.11.14': resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} @@ -2698,6 +2702,7 @@ packages: '@humanwhocodes/object-schema@2.0.2': resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} + deprecated: Use @eslint/object-schema instead '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} @@ -10011,6 +10016,12 @@ packages: resolution: {integrity: sha512-fUeWjrkOO0t1rg7B2fdyDTvngj+9RlUyL92vOdiB7c0FPguWVsniIMjEtHH+meLBO9rzkUlUzBVXgWrjI8P9LA==} engines: {node: '>=12'} + re-resizable@6.9.17: + resolution: {integrity: sha512-OBqd1BwVXpEJJn/yYROG+CbeqIDBWIp6wathlpB0kzZWWZIY1gPTsgK2yJEui5hOvkCdC2mcexF2V3DZVfLq2g==} + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-ace@10.1.0: resolution: {integrity: sha512-VkvUjZNhdYTuKOKQpMIZi7uzZZVgzCjM7cLYu6F64V0mejY8a2XTyPUIMszC6A4trbeMIHbK5fYFcT/wkP/8VA==} peerDependencies: @@ -12741,27 +12752,27 @@ snapshots: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 - '@api-platform/api-doc-parser@0.16.2': + '@api-platform/api-doc-parser@0.16.2(web-streams-polyfill@3.3.3)': dependencies: graphql: 16.8.1 inflection: 1.13.4 - jsonld: 8.3.2 + jsonld: 8.3.2(web-streams-polyfill@3.3.3) jsonref: 8.0.8 lodash.get: 4.4.2 tslib: 2.6.2 transitivePeerDependencies: - web-streams-polyfill - '@api-platform/create-client@0.10.0': + '@api-platform/create-client@0.10.0(encoding@0.1.13)(web-streams-polyfill@3.3.3)': dependencies: - '@api-platform/api-doc-parser': 0.16.2 + '@api-platform/api-doc-parser': 0.16.2(web-streams-polyfill@3.3.3) '@babel/runtime': 7.24.0 chalk: 5.3.0 commander: 10.0.1 esutils: 2.0.3 handlebars: 4.7.8 handlebars-helpers: 0.10.0 - isomorphic-fetch: 3.0.0 + isomorphic-fetch: 3.0.0(encoding@0.1.13) mkdirp: 2.1.6 prettier: 2.8.8 sprintf-js: 1.1.3 @@ -13676,10 +13687,10 @@ snapshots: enabled: 2.0.0 kuler: 2.0.0 - '@digitalbazaar/http-client@3.4.1': + '@digitalbazaar/http-client@3.4.1(web-streams-polyfill@3.3.3)': dependencies: ky: 0.33.3 - ky-universal: 0.11.0(ky@0.33.3) + ky-universal: 0.11.0(ky@0.33.3)(web-streams-polyfill@3.3.3) undici: 5.28.3 transitivePeerDependencies: - web-streams-polyfill @@ -13691,7 +13702,7 @@ snapshots: react: 18.2.0 tslib: 2.6.2 - '@dnd-kit/core@6.1.0(react-dom@18.2.0)(react@18.2.0)': + '@dnd-kit/core@6.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@dnd-kit/accessibility': 3.1.0(react@18.2.0) '@dnd-kit/utilities': 3.2.2(react@18.2.0) @@ -13699,9 +13710,9 @@ snapshots: react-dom: 18.2.0(react@18.2.0) tslib: 2.6.2 - '@dnd-kit/sortable@7.0.2(@dnd-kit/core@6.1.0)(react@18.2.0)': + '@dnd-kit/sortable@7.0.2(@dnd-kit/core@6.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0)': dependencies: - '@dnd-kit/core': 6.1.0(react-dom@18.2.0)(react@18.2.0) + '@dnd-kit/core': 6.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@dnd-kit/utilities': 3.2.2(react@18.2.0) react: 18.2.0 tslib: 2.6.2 @@ -13754,9 +13765,10 @@ snapshots: '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) '@emotion/utils': 1.2.1 '@emotion/weak-memoize': 0.3.1 - '@types/react': 18.2.65 hoist-non-react-statics: 3.3.2 react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.65 '@emotion/serialize@1.1.3': dependencies: @@ -13768,7 +13780,7 @@ snapshots: '@emotion/sheet@1.2.2': {} - '@emotion/styled@11.11.0(@emotion/react@11.11.4)(@types/react@18.2.65)(react@18.2.0)': + '@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0)': dependencies: '@babel/runtime': 7.24.0 '@emotion/babel-plugin': 11.11.0 @@ -13777,8 +13789,9 @@ snapshots: '@emotion/serialize': 1.1.3 '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) '@emotion/utils': 1.2.1 - '@types/react': 18.2.65 react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.65 '@emotion/unitless@0.8.0': {} @@ -14032,7 +14045,7 @@ snapshots: '@floating-ui/core': 1.6.0 '@floating-ui/utils': 0.2.1 - '@floating-ui/react-dom@2.0.8(react-dom@18.2.0)(react@18.2.0)': + '@floating-ui/react-dom@2.0.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@floating-ui/dom': 1.6.3 react: 18.2.0 @@ -14147,14 +14160,15 @@ snapshots: '@jonkoops/matomo-tracker@0.7.0': {} - '@joshwooding/vite-plugin-react-docgen-typescript@0.3.0(typescript@5.4.2)(vite@5.1.6)': + '@joshwooding/vite-plugin-react-docgen-typescript@0.3.0(typescript@5.4.2)(vite@5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3))': dependencies: glob: 7.2.3 glob-promise: 4.2.2(glob@7.2.3) magic-string: 0.27.0 react-docgen-typescript: 2.2.2(typescript@5.4.2) + vite: 5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3) + optionalDependencies: typescript: 5.4.2 - vite: 5.1.6(@types/node@18.19.24)(sass@1.71.1) '@jridgewell/gen-mapping@0.3.5': dependencies: @@ -14198,12 +14212,12 @@ snapshots: dependencies: mapbox-gl: 1.13.3 - '@mapbox/node-pre-gyp@1.0.11': + '@mapbox/node-pre-gyp@1.0.11(encoding@0.1.13)': dependencies: detect-libc: 2.0.2 https-proxy-agent: 5.0.1 make-dir: 3.1.0 - node-fetch: 2.7.0 + node-fetch: 2.7.0(encoding@0.1.13) nopt: 5.0.0 npmlog: 5.0.1 rimraf: 3.0.2 @@ -14277,55 +14291,55 @@ snapshots: '@bundled-es-modules/pdfjs-dist': 2.16.106 react: 18.2.0 - '@mui/base@5.0.0-beta.39(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@mui/base@5.0.0-beta.39(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 - '@floating-ui/react-dom': 2.0.8(react-dom@18.2.0)(react@18.2.0) + '@floating-ui/react-dom': 2.0.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@mui/types': 7.2.14(@types/react@18.2.65) '@mui/utils': 5.15.14(@types/react@18.2.65)(react@18.2.0) '@popperjs/core': 2.11.8 - '@types/react': 18.2.65 clsx: 2.1.0 prop-types: 15.8.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.65 '@mui/core-downloads-tracker@5.15.13': {} - '@mui/icons-material@5.15.13(@mui/material@5.15.13)(@types/react@18.2.65)(react@18.2.0)': + '@mui/icons-material@5.15.13(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.2.65)(react@18.2.0)': dependencies: '@babel/runtime': 7.24.0 - '@mui/material': 5.15.13(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) - '@types/react': 18.2.65 + '@mui/material': 5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.65 - '@mui/lab@5.0.0-alpha.168(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@mui/material@5.15.13)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@mui/lab@5.0.0-alpha.168(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 - '@emotion/react': 11.11.4(@types/react@18.2.65)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.65)(react@18.2.0) - '@mui/base': 5.0.0-beta.39(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) - '@mui/material': 5.15.13(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) - '@mui/system': 5.15.15(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.65)(react@18.2.0) + '@mui/base': 5.0.0-beta.39(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@mui/material': 5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@mui/system': 5.15.15(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0) '@mui/types': 7.2.14(@types/react@18.2.65) '@mui/utils': 5.15.14(@types/react@18.2.65)(react@18.2.0) - '@types/react': 18.2.65 clsx: 2.1.0 prop-types: 15.8.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@emotion/react': 11.11.4(@types/react@18.2.65)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0) + '@types/react': 18.2.65 - '@mui/material@5.15.13(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 - '@emotion/react': 11.11.4(@types/react@18.2.65)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.65)(react@18.2.0) - '@mui/base': 5.0.0-beta.39(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@mui/base': 5.0.0-beta.39(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@mui/core-downloads-tracker': 5.15.13 - '@mui/system': 5.15.15(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.65)(react@18.2.0) + '@mui/system': 5.15.15(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0) '@mui/types': 7.2.14(@types/react@18.2.65) '@mui/utils': 5.15.14(@types/react@18.2.65)(react@18.2.0) - '@types/react': 18.2.65 '@types/react-transition-group': 4.4.10 clsx: 2.1.0 csstype: 3.1.3 @@ -14333,78 +14347,87 @@ snapshots: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) react-is: 18.2.0 - react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + optionalDependencies: + '@emotion/react': 11.11.4(@types/react@18.2.65)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0) + '@types/react': 18.2.65 '@mui/private-theming@5.15.14(@types/react@18.2.65)(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 '@mui/utils': 5.15.14(@types/react@18.2.65)(react@18.2.0) - '@types/react': 18.2.65 prop-types: 15.8.1 react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.65 - '@mui/styled-engine@5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0)': + '@mui/styled-engine@5.15.14(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 '@emotion/cache': 11.11.0 - '@emotion/react': 11.11.4(@types/react@18.2.65)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.65)(react@18.2.0) csstype: 3.1.3 prop-types: 15.8.1 react: 18.2.0 + optionalDependencies: + '@emotion/react': 11.11.4(@types/react@18.2.65)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0) - '@mui/system@5.15.15(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.65)(react@18.2.0)': + '@mui/system@5.15.15(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 - '@emotion/react': 11.11.4(@types/react@18.2.65)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.65)(react@18.2.0) '@mui/private-theming': 5.15.14(@types/react@18.2.65)(react@18.2.0) - '@mui/styled-engine': 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(react@18.2.0) + '@mui/styled-engine': 5.15.14(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(react@18.2.0) '@mui/types': 7.2.14(@types/react@18.2.65) '@mui/utils': 5.15.14(@types/react@18.2.65)(react@18.2.0) - '@types/react': 18.2.65 clsx: 2.1.0 csstype: 3.1.3 prop-types: 15.8.1 react: 18.2.0 + optionalDependencies: + '@emotion/react': 11.11.4(@types/react@18.2.65)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0) + '@types/react': 18.2.65 '@mui/types@7.2.14(@types/react@18.2.65)': - dependencies: + optionalDependencies: '@types/react': 18.2.65 '@mui/utils@5.15.13(@types/react@18.2.65)(react@18.2.0)': dependencies: '@babel/runtime': 7.24.0 '@types/prop-types': 15.7.11 - '@types/react': 18.2.65 prop-types: 15.8.1 react: 18.2.0 react-is: 18.2.0 + optionalDependencies: + '@types/react': 18.2.65 '@mui/utils@5.15.14(@types/react@18.2.65)(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 '@types/prop-types': 15.7.12 - '@types/react': 18.2.65 prop-types: 15.8.1 react: 18.2.0 react-is: 18.2.0 + optionalDependencies: + '@types/react': 18.2.65 - '@mui/x-tree-view@6.17.0(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@mui/material@5.15.13)(@mui/system@5.15.15)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@mui/x-tree-view@6.17.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mui/system@5.15.15(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.24.0 '@emotion/react': 11.11.4(@types/react@18.2.65)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.65)(react@18.2.0) - '@mui/base': 5.0.0-beta.39(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) - '@mui/material': 5.15.13(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) - '@mui/system': 5.15.15(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.65)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0) + '@mui/base': 5.0.0-beta.39(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@mui/material': 5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@mui/system': 5.15.15(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0) '@mui/utils': 5.15.13(@types/react@18.2.65)(react@18.2.0) '@types/react-transition-group': 4.4.10 clsx: 2.1.0 prop-types: 15.8.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) transitivePeerDependencies: - '@types/react' @@ -14492,13 +14515,13 @@ snapshots: dependencies: '@octokit/types': 13.4.1 - '@octokit/request@5.6.3': + '@octokit/request@5.6.3(encoding@0.1.13)': dependencies: '@octokit/endpoint': 6.0.12 '@octokit/request-error': 2.1.0 '@octokit/types': 6.41.0 is-plain-object: 5.0.0 - node-fetch: 2.7.0 + node-fetch: 2.7.0(encoding@0.1.13) universal-user-agent: 6.0.1 transitivePeerDependencies: - encoding @@ -14510,13 +14533,13 @@ snapshots: '@octokit/types': 13.4.1 universal-user-agent: 7.0.2 - '@octokit/rest@16.43.2(@octokit/core@6.1.2)': + '@octokit/rest@16.43.2(@octokit/core@6.1.2)(encoding@0.1.13)': dependencies: '@octokit/auth-token': 2.5.0 '@octokit/plugin-paginate-rest': 1.1.2 '@octokit/plugin-request-log': 1.0.4(@octokit/core@6.1.2) '@octokit/plugin-rest-endpoint-methods': 2.4.0 - '@octokit/request': 5.6.3 + '@octokit/request': 5.6.3(encoding@0.1.13) '@octokit/request-error': 1.2.1 atob-lite: 2.0.0 before-after-hook: 2.2.3 @@ -14557,275 +14580,302 @@ snapshots: dependencies: '@babel/runtime': 7.24.4 - '@radix-ui/react-arrow@1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@radix-ui/react-arrow@1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) - '@types/react': 18.2.65 - '@types/react-dom': 18.2.22 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.65 + '@types/react-dom': 18.2.22 - '@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.65)(react@18.2.0) '@radix-ui/react-context': 1.0.1(@types/react@18.2.65)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-slot': 1.0.2(@types/react@18.2.65)(react@18.2.0) - '@types/react': 18.2.65 - '@types/react-dom': 18.2.22 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.65 + '@types/react-dom': 18.2.22 '@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.65)(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 - '@types/react': 18.2.65 react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.65 '@radix-ui/react-context@1.0.1(@types/react@18.2.65)(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 - '@types/react': 18.2.65 react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.65 '@radix-ui/react-direction@1.0.1(@types/react@18.2.65)(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 - '@types/react': 18.2.65 react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.65 - '@radix-ui/react-dismissable-layer@1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@radix-ui/react-dismissable-layer@1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.65)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.65)(react@18.2.0) '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.2.65)(react@18.2.0) - '@types/react': 18.2.65 - '@types/react-dom': 18.2.22 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.65 + '@types/react-dom': 18.2.22 '@radix-ui/react-focus-guards@1.0.1(@types/react@18.2.65)(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 - '@types/react': 18.2.65 react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.65 - '@radix-ui/react-focus-scope@1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@radix-ui/react-focus-scope@1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.65)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.65)(react@18.2.0) - '@types/react': 18.2.65 - '@types/react-dom': 18.2.22 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.65 + '@types/react-dom': 18.2.22 '@radix-ui/react-id@1.0.1(@types/react@18.2.65)(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.65)(react@18.2.0) - '@types/react': 18.2.65 react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.65 - '@radix-ui/react-popper@1.1.2(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@radix-ui/react-popper@1.1.2(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 - '@floating-ui/react-dom': 2.0.8(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@floating-ui/react-dom': 2.0.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.65)(react@18.2.0) '@radix-ui/react-context': 1.0.1(@types/react@18.2.65)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.65)(react@18.2.0) '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.65)(react@18.2.0) '@radix-ui/react-use-rect': 1.0.1(@types/react@18.2.65)(react@18.2.0) '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.65)(react@18.2.0) '@radix-ui/rect': 1.0.1 - '@types/react': 18.2.65 - '@types/react-dom': 18.2.22 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.65 + '@types/react-dom': 18.2.22 - '@radix-ui/react-portal@1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@radix-ui/react-portal@1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) - '@types/react': 18.2.65 - '@types/react-dom': 18.2.22 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.65 + '@types/react-dom': 18.2.22 - '@radix-ui/react-primitive@1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@radix-ui/react-primitive@1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 '@radix-ui/react-slot': 1.0.2(@types/react@18.2.65)(react@18.2.0) - '@types/react': 18.2.65 - '@types/react-dom': 18.2.22 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.65 + '@types/react-dom': 18.2.22 - '@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.65)(react@18.2.0) '@radix-ui/react-context': 1.0.1(@types/react@18.2.65)(react@18.2.0) '@radix-ui/react-direction': 1.0.1(@types/react@18.2.65)(react@18.2.0) '@radix-ui/react-id': 1.0.1(@types/react@18.2.65)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.65)(react@18.2.0) '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.65)(react@18.2.0) - '@types/react': 18.2.65 - '@types/react-dom': 18.2.22 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.65 + '@types/react-dom': 18.2.22 - '@radix-ui/react-select@1.2.2(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@radix-ui/react-select@1.2.2(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 '@radix-ui/number': 1.0.1 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.65)(react@18.2.0) '@radix-ui/react-context': 1.0.1(@types/react@18.2.65)(react@18.2.0) '@radix-ui/react-direction': 1.0.1(@types/react@18.2.65)(react@18.2.0) - '@radix-ui/react-dismissable-layer': 1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.65)(react@18.2.0) - '@radix-ui/react-focus-scope': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-id': 1.0.1(@types/react@18.2.65)(react@18.2.0) - '@radix-ui/react-popper': 1.1.2(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-portal': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-popper': 1.1.2(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-portal': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-slot': 1.0.2(@types/react@18.2.65)(react@18.2.0) '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.65)(react@18.2.0) '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.65)(react@18.2.0) '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.65)(react@18.2.0) '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.65)(react@18.2.0) - '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) - '@types/react': 18.2.65 - '@types/react-dom': 18.2.22 + '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) aria-hidden: 1.2.3 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) react-remove-scroll: 2.5.5(@types/react@18.2.65)(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.65 + '@types/react-dom': 18.2.22 - '@radix-ui/react-separator@1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@radix-ui/react-separator@1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) - '@types/react': 18.2.65 - '@types/react-dom': 18.2.22 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.65 + '@types/react-dom': 18.2.22 '@radix-ui/react-slot@1.0.2(@types/react@18.2.65)(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.65)(react@18.2.0) - '@types/react': 18.2.65 react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.65 - '@radix-ui/react-toggle-group@1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@radix-ui/react-toggle-group@1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-context': 1.0.1(@types/react@18.2.65)(react@18.2.0) '@radix-ui/react-direction': 1.0.1(@types/react@18.2.65)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-toggle': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-toggle': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.65)(react@18.2.0) - '@types/react': 18.2.65 - '@types/react-dom': 18.2.22 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.65 + '@types/react-dom': 18.2.22 - '@radix-ui/react-toggle@1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@radix-ui/react-toggle@1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.65)(react@18.2.0) - '@types/react': 18.2.65 - '@types/react-dom': 18.2.22 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.65 + '@types/react-dom': 18.2.22 - '@radix-ui/react-toolbar@1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@radix-ui/react-toolbar@1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-context': 1.0.1(@types/react@18.2.65)(react@18.2.0) '@radix-ui/react-direction': 1.0.1(@types/react@18.2.65)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-separator': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-toggle-group': 1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) - '@types/react': 18.2.65 - '@types/react-dom': 18.2.22 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-separator': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-toggle-group': 1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.65 + '@types/react-dom': 18.2.22 '@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.2.65)(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 - '@types/react': 18.2.65 react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.65 '@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.2.65)(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.65)(react@18.2.0) - '@types/react': 18.2.65 react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.65 '@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.2.65)(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.65)(react@18.2.0) - '@types/react': 18.2.65 react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.65 '@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.2.65)(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 - '@types/react': 18.2.65 react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.65 '@radix-ui/react-use-previous@1.0.1(@types/react@18.2.65)(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 - '@types/react': 18.2.65 react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.65 '@radix-ui/react-use-rect@1.0.1(@types/react@18.2.65)(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 '@radix-ui/rect': 1.0.1 - '@types/react': 18.2.65 react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.65 '@radix-ui/react-use-size@1.0.1(@types/react@18.2.65)(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.65)(react@18.2.0) - '@types/react': 18.2.65 react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.65 - '@radix-ui/react-visually-hidden@1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@radix-ui/react-visually-hidden@1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) - '@types/react': 18.2.65 - '@types/react-dom': 18.2.22 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.65 + '@types/react-dom': 18.2.22 '@radix-ui/rect@1.0.1': dependencies: @@ -14837,35 +14887,35 @@ snapshots: '@react-dnd/shallowequal@4.0.2': {} - '@react-leaflet/core@2.1.0(leaflet@1.9.4)(react-dom@18.2.0)(react@18.2.0)': + '@react-leaflet/core@2.1.0(leaflet@1.9.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: leaflet: 1.9.4 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - '@reactflow/background@11.3.9(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@reactflow/background@11.3.9(@types/react@18.2.65)(immer@10.0.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.65)(immer@10.0.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classcat: 5.0.4 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.2(@types/react@18.2.65)(react@18.2.0) + zustand: 4.5.2(@types/react@18.2.65)(immer@10.0.4)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/controls@11.2.9(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@reactflow/controls@11.2.9(@types/react@18.2.65)(immer@10.0.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.65)(immer@10.0.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classcat: 5.0.4 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.2(@types/react@18.2.65)(react@18.2.0) + zustand: 4.5.2(@types/react@18.2.65)(immer@10.0.4)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/core@11.10.4(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@reactflow/core@11.10.4(@types/react@18.2.65)(immer@10.0.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@types/d3': 7.4.3 '@types/d3-drag': 3.0.7 @@ -14877,14 +14927,14 @@ snapshots: d3-zoom: 3.0.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.2(@types/react@18.2.65)(react@18.2.0) + zustand: 4.5.2(@types/react@18.2.65)(immer@10.0.4)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/minimap@11.7.9(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@reactflow/minimap@11.7.9(@types/react@18.2.65)(immer@10.0.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.65)(immer@10.0.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@types/d3-selection': 3.0.10 '@types/d3-zoom': 3.0.8 classcat: 5.0.4 @@ -14892,43 +14942,44 @@ snapshots: d3-zoom: 3.0.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.2(@types/react@18.2.65)(react@18.2.0) + zustand: 4.5.2(@types/react@18.2.65)(immer@10.0.4)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/node-resizer@2.2.9(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@reactflow/node-resizer@2.2.9(@types/react@18.2.65)(immer@10.0.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.65)(immer@10.0.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classcat: 5.0.4 d3-drag: 3.0.0 d3-selection: 3.0.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.2(@types/react@18.2.65)(react@18.2.0) + zustand: 4.5.2(@types/react@18.2.65)(immer@10.0.4)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/node-toolbar@1.3.9(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@reactflow/node-toolbar@1.3.9(@types/react@18.2.65)(immer@10.0.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.65)(immer@10.0.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) classcat: 5.0.4 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.2(@types/react@18.2.65)(react@18.2.0) + zustand: 4.5.2(@types/react@18.2.65)(immer@10.0.4)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer - '@reduxjs/toolkit@2.2.1(react-redux@9.1.0)(react@18.2.0)': + '@reduxjs/toolkit@2.2.1(react-redux@9.1.0(@types/react@18.2.65)(react@18.2.0)(redux@4.2.1))(react@18.2.0)': dependencies: immer: 10.0.4 - react: 18.2.0 - react-redux: 9.1.0(@types/react@18.2.65)(react@18.2.0)(redux@4.2.1) redux: 5.0.1 redux-thunk: 3.1.0(redux@5.0.1) reselect: 5.1.0 + optionalDependencies: + react: 18.2.0 + react-redux: 9.1.0(@types/react@18.2.65)(react@18.2.0)(redux@4.2.1) '@remix-run/router@1.11.0': {} @@ -14946,11 +14997,13 @@ snapshots: estree-walker: 2.0.2 picomatch: 2.3.1 - '@rollup/pluginutils@5.1.0': + '@rollup/pluginutils@5.1.0(rollup@4.13.0)': dependencies: '@types/estree': 1.0.5 estree-walker: 2.0.2 picomatch: 2.3.1 + optionalDependencies: + rollup: 4.13.0 '@rollup/rollup-android-arm-eabi@4.13.0': optional: true @@ -14993,7 +15046,6 @@ snapshots: '@rushstack/node-core-library@3.62.0(@types/node@18.19.24)': dependencies: - '@types/node': 18.19.24 colors: 1.2.5 fs-extra: 7.0.1 import-lazy: 4.0.0 @@ -15001,6 +15053,8 @@ snapshots: resolve: 1.22.8 semver: 7.5.4 z-schema: 5.0.5 + optionalDependencies: + '@types/node': 18.19.24 '@rushstack/rig-package@0.5.1': dependencies: @@ -15017,11 +15071,12 @@ snapshots: '@samverschueren/stream-to-observable@0.3.1(rxjs@6.6.7)': dependencies: any-observable: 0.3.0(rxjs@6.6.7) + optionalDependencies: rxjs: 6.6.7 transitivePeerDependencies: - zenObservable - '@semantic-release/commit-analyzer@6.3.3(semantic-release@15.14.0)': + '@semantic-release/commit-analyzer@6.3.3(semantic-release@15.14.0(@octokit/core@6.1.2)(encoding@0.1.13))': dependencies: conventional-changelog-angular: 5.0.13 conventional-commits-filter: 2.0.7 @@ -15029,15 +15084,15 @@ snapshots: debug: 4.3.4(supports-color@5.5.0) import-from: 3.0.0 lodash: 4.17.21 - semantic-release: 15.14.0(@octokit/core@6.1.2) + semantic-release: 15.14.0(@octokit/core@6.1.2)(encoding@0.1.13) transitivePeerDependencies: - supports-color '@semantic-release/error@2.2.0': {} - '@semantic-release/github@5.5.8(@octokit/core@6.1.2)(semantic-release@15.14.0)': + '@semantic-release/github@5.5.8(@octokit/core@6.1.2)(encoding@0.1.13)(semantic-release@15.14.0(@octokit/core@6.1.2)(encoding@0.1.13))': dependencies: - '@octokit/rest': 16.43.2(@octokit/core@6.1.2) + '@octokit/rest': 16.43.2(@octokit/core@6.1.2)(encoding@0.1.13) '@semantic-release/error': 2.2.0 aggregate-error: 3.1.0 bottleneck: 2.19.5 @@ -15052,14 +15107,14 @@ snapshots: mime: 2.6.0 p-filter: 2.1.0 p-retry: 4.6.2 - semantic-release: 15.14.0(@octokit/core@6.1.2) + semantic-release: 15.14.0(@octokit/core@6.1.2)(encoding@0.1.13) url-join: 4.0.1 transitivePeerDependencies: - '@octokit/core' - encoding - supports-color - '@semantic-release/npm@5.3.5(semantic-release@15.14.0)': + '@semantic-release/npm@5.3.5(semantic-release@15.14.0(@octokit/core@6.1.2)(encoding@0.1.13))': dependencies: '@semantic-release/error': 2.2.0 aggregate-error: 3.1.0 @@ -15072,10 +15127,10 @@ snapshots: rc: 1.2.8 read-pkg: 5.2.0 registry-auth-token: 4.2.2 - semantic-release: 15.14.0(@octokit/core@6.1.2) + semantic-release: 15.14.0(@octokit/core@6.1.2)(encoding@0.1.13) tempy: 0.3.0 - '@semantic-release/release-notes-generator@7.3.5(semantic-release@15.14.0)': + '@semantic-release/release-notes-generator@7.3.5(semantic-release@15.14.0(@octokit/core@6.1.2)(encoding@0.1.13))': dependencies: conventional-changelog-angular: 5.0.13 conventional-changelog-writer: 4.1.0 @@ -15087,7 +15142,7 @@ snapshots: into-stream: 5.1.1 lodash: 4.17.21 read-pkg-up: 7.0.1 - semantic-release: 15.14.0(@octokit/core@6.1.2) + semantic-release: 15.14.0(@octokit/core@6.1.2)(encoding@0.1.13) transitivePeerDependencies: - supports-color @@ -15174,9 +15229,9 @@ snapshots: memoizerific: 1.11.3 ts-dedent: 2.2.0 - '@storybook/addon-controls@7.6.17(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@storybook/addon-controls@7.6.17(@types/react-dom@18.2.22)(@types/react@18.2.65)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@storybook/blocks': 7.6.17(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@storybook/blocks': 7.6.17(@types/react-dom@18.2.22)(@types/react@18.2.65)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) lodash: 4.17.21 ts-dedent: 2.2.0 transitivePeerDependencies: @@ -15187,13 +15242,13 @@ snapshots: - react-dom - supports-color - '@storybook/addon-docs@7.6.17(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@storybook/addon-docs@7.6.17(@types/react-dom@18.2.22)(@types/react@18.2.65)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@jest/transform': 29.7.0 '@mdx-js/react': 2.3.0(react@18.2.0) - '@storybook/blocks': 7.6.17(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@storybook/blocks': 7.6.17(@types/react-dom@18.2.22)(@types/react@18.2.65)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@storybook/client-logger': 7.6.17 - '@storybook/components': 7.6.17(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@storybook/components': 7.6.17(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@storybook/csf-plugin': 7.6.17 '@storybook/csf-tools': 7.6.17 '@storybook/global': 5.0.0 @@ -15201,8 +15256,8 @@ snapshots: '@storybook/node-logger': 7.6.17 '@storybook/postinstall': 7.6.17 '@storybook/preview-api': 7.6.17 - '@storybook/react-dom-shim': 7.6.17(react-dom@18.2.0)(react@18.2.0) - '@storybook/theming': 7.6.17(react-dom@18.2.0)(react@18.2.0) + '@storybook/react-dom-shim': 7.6.17(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@storybook/theming': 7.6.17(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@storybook/types': 7.6.17 fs-extra: 11.2.0 react: 18.2.0 @@ -15216,19 +15271,19 @@ snapshots: - encoding - supports-color - '@storybook/addon-essentials@7.6.17(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@storybook/addon-essentials@7.6.17(@types/react-dom@18.2.22)(@types/react@18.2.65)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@storybook/addon-actions': 7.6.17 '@storybook/addon-backgrounds': 7.6.17 - '@storybook/addon-controls': 7.6.17(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) - '@storybook/addon-docs': 7.6.17(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@storybook/addon-controls': 7.6.17(@types/react-dom@18.2.22)(@types/react@18.2.65)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@storybook/addon-docs': 7.6.17(@types/react-dom@18.2.22)(@types/react@18.2.65)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@storybook/addon-highlight': 7.6.17 '@storybook/addon-measure': 7.6.17 '@storybook/addon-outline': 7.6.17 '@storybook/addon-toolbars': 7.6.17 '@storybook/addon-viewport': 7.6.17 - '@storybook/core-common': 7.6.17 - '@storybook/manager-api': 7.6.17(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-common': 7.6.17(encoding@0.1.13) + '@storybook/manager-api': 7.6.17(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@storybook/node-logger': 7.6.17 '@storybook/preview-api': 7.6.17 react: 18.2.0 @@ -15256,8 +15311,9 @@ snapshots: dependencies: '@storybook/csf': 0.1.2 '@storybook/global': 5.0.0 - react: 18.2.0 ts-dedent: 2.2.0 + optionalDependencies: + react: 18.2.0 '@storybook/addon-measure@7.6.17': dependencies: @@ -15275,18 +15331,18 @@ snapshots: dependencies: memoizerific: 1.11.3 - '@storybook/blocks@7.6.17(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@storybook/blocks@7.6.17(@types/react-dom@18.2.22)(@types/react@18.2.65)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@storybook/channels': 7.6.17 '@storybook/client-logger': 7.6.17 - '@storybook/components': 7.6.17(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@storybook/components': 7.6.17(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@storybook/core-events': 7.6.17 '@storybook/csf': 0.1.2 - '@storybook/docs-tools': 7.6.17 + '@storybook/docs-tools': 7.6.17(encoding@0.1.13) '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.6.17(react-dom@18.2.0)(react@18.2.0) + '@storybook/manager-api': 7.6.17(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@storybook/preview-api': 7.6.17 - '@storybook/theming': 7.6.17(react-dom@18.2.0)(react@18.2.0) + '@storybook/theming': 7.6.17(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@storybook/types': 7.6.17 '@types/lodash': 4.17.0 color-convert: 2.0.1 @@ -15296,7 +15352,7 @@ snapshots: memoizerific: 1.11.3 polished: 4.3.1 react: 18.2.0 - react-colorful: 5.6.1(react-dom@18.2.0)(react@18.2.0) + react-colorful: 5.6.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-dom: 18.2.0(react@18.2.0) telejson: 7.2.0 tocbot: 4.25.0 @@ -15308,10 +15364,10 @@ snapshots: - encoding - supports-color - '@storybook/builder-manager@7.6.17': + '@storybook/builder-manager@7.6.17(encoding@0.1.13)': dependencies: '@fal-works/esbuild-plugin-global-externals': 2.1.2 - '@storybook/core-common': 7.6.17 + '@storybook/core-common': 7.6.17(encoding@0.1.13) '@storybook/manager': 7.6.17 '@storybook/node-logger': 7.6.17 '@types/ejs': 3.1.5 @@ -15330,11 +15386,11 @@ snapshots: - encoding - supports-color - '@storybook/builder-vite@7.6.17(typescript@5.4.2)(vite@5.1.6)': + '@storybook/builder-vite@7.6.17(encoding@0.1.13)(typescript@5.4.2)(vite@5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3))': dependencies: '@storybook/channels': 7.6.17 '@storybook/client-logger': 7.6.17 - '@storybook/core-common': 7.6.17 + '@storybook/core-common': 7.6.17(encoding@0.1.13) '@storybook/csf-plugin': 7.6.17 '@storybook/node-logger': 7.6.17 '@storybook/preview': 7.6.17 @@ -15348,8 +15404,9 @@ snapshots: fs-extra: 11.2.0 magic-string: 0.30.8 rollup: 3.29.4 + vite: 5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3) + optionalDependencies: typescript: 5.4.2 - vite: 5.1.6(@types/node@18.19.24)(sass@1.71.1) transitivePeerDependencies: - encoding - supports-color @@ -15363,19 +15420,19 @@ snapshots: telejson: 7.2.0 tiny-invariant: 1.3.3 - '@storybook/cli@7.6.17': + '@storybook/cli@7.6.17(encoding@0.1.13)': dependencies: '@babel/core': 7.24.0 '@babel/preset-env': 7.24.0(@babel/core@7.24.0) '@babel/types': 7.24.0 '@ndelangen/get-tarball': 3.0.9 '@storybook/codemod': 7.6.17 - '@storybook/core-common': 7.6.17 + '@storybook/core-common': 7.6.17(encoding@0.1.13) '@storybook/core-events': 7.6.17 - '@storybook/core-server': 7.6.17 + '@storybook/core-server': 7.6.17(encoding@0.1.13) '@storybook/csf-tools': 7.6.17 '@storybook/node-logger': 7.6.17 - '@storybook/telemetry': 7.6.17 + '@storybook/telemetry': 7.6.17(encoding@0.1.13) '@storybook/types': 7.6.17 '@types/semver': 7.5.8 '@yarnpkg/fslib': 2.10.3 @@ -15393,7 +15450,7 @@ snapshots: get-port: 5.1.1 giget: 1.2.1 globby: 11.1.0 - jscodeshift: 0.15.2(@babel/preset-env@7.24.0) + jscodeshift: 0.15.2(@babel/preset-env@7.24.0(@babel/core@7.24.0)) leven: 3.1.0 ora: 5.4.1 prettier: 2.8.8 @@ -15427,26 +15484,26 @@ snapshots: '@types/cross-spawn': 6.0.6 cross-spawn: 7.0.3 globby: 11.1.0 - jscodeshift: 0.15.2(@babel/preset-env@7.24.0) + jscodeshift: 0.15.2(@babel/preset-env@7.24.0(@babel/core@7.24.0)) lodash: 4.17.21 prettier: 2.8.8 recast: 0.23.6 transitivePeerDependencies: - supports-color - '@storybook/components@7.6.17(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@storybook/components@7.6.17(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@radix-ui/react-select': 1.2.2(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-toolbar': 1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-select': 1.2.2(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-toolbar': 1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@storybook/client-logger': 7.6.17 '@storybook/csf': 0.1.2 '@storybook/global': 5.0.0 - '@storybook/theming': 7.6.17(react-dom@18.2.0)(react@18.2.0) + '@storybook/theming': 7.6.17(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@storybook/types': 7.6.17 memoizerific: 1.11.3 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - use-resize-observer: 9.1.0(react-dom@18.2.0)(react@18.2.0) + use-resize-observer: 9.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) util-deprecate: 1.0.2 transitivePeerDependencies: - '@types/react' @@ -15457,7 +15514,7 @@ snapshots: '@storybook/client-logger': 7.6.17 '@storybook/preview-api': 7.6.17 - '@storybook/core-common@7.6.17': + '@storybook/core-common@7.6.17(encoding@0.1.13)': dependencies: '@storybook/core-events': 7.6.17 '@storybook/node-logger': 7.6.17 @@ -15476,7 +15533,7 @@ snapshots: glob: 10.3.10 handlebars: 4.7.8 lazy-universal-dotenv: 4.0.0 - node-fetch: 2.7.0 + node-fetch: 2.7.0(encoding@0.1.13) picomatch: 2.3.1 pkg-dir: 5.0.0 pretty-hrtime: 1.0.3 @@ -15490,13 +15547,13 @@ snapshots: dependencies: ts-dedent: 2.2.0 - '@storybook/core-server@7.6.17': + '@storybook/core-server@7.6.17(encoding@0.1.13)': dependencies: '@aw-web-design/x-default-browser': 1.4.126 '@discoveryjs/json-ext': 0.5.7 - '@storybook/builder-manager': 7.6.17 + '@storybook/builder-manager': 7.6.17(encoding@0.1.13) '@storybook/channels': 7.6.17 - '@storybook/core-common': 7.6.17 + '@storybook/core-common': 7.6.17(encoding@0.1.13) '@storybook/core-events': 7.6.17 '@storybook/csf': 0.1.2 '@storybook/csf-tools': 7.6.17 @@ -15505,7 +15562,7 @@ snapshots: '@storybook/manager': 7.6.17 '@storybook/node-logger': 7.6.17 '@storybook/preview-api': 7.6.17 - '@storybook/telemetry': 7.6.17 + '@storybook/telemetry': 7.6.17(encoding@0.1.13) '@storybook/types': 7.6.17 '@types/detect-port': 1.3.5 '@types/node': 18.19.24 @@ -15566,9 +15623,9 @@ snapshots: '@storybook/docs-mdx@0.1.0': {} - '@storybook/docs-tools@7.6.17': + '@storybook/docs-tools@7.6.17(encoding@0.1.13)': dependencies: - '@storybook/core-common': 7.6.17 + '@storybook/core-common': 7.6.17(encoding@0.1.13) '@storybook/preview-api': 7.6.17 '@storybook/types': 7.6.17 '@types/doctrine': 0.0.3 @@ -15591,7 +15648,7 @@ snapshots: '@vitest/utils': 0.34.7 util: 0.12.5 - '@storybook/manager-api@7.6.17(react-dom@18.2.0)(react@18.2.0)': + '@storybook/manager-api@7.6.17(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@storybook/channels': 7.6.17 '@storybook/client-logger': 7.6.17 @@ -15599,7 +15656,7 @@ snapshots: '@storybook/csf': 0.1.2 '@storybook/global': 5.0.0 '@storybook/router': 7.6.17 - '@storybook/theming': 7.6.17(react-dom@18.2.0)(react@18.2.0) + '@storybook/theming': 7.6.17(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@storybook/types': 7.6.17 dequal: 2.0.3 lodash: 4.17.21 @@ -15638,23 +15695,23 @@ snapshots: '@storybook/preview@7.6.17': {} - '@storybook/react-dom-shim@7.6.17(react-dom@18.2.0)(react@18.2.0)': + '@storybook/react-dom-shim@7.6.17(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - '@storybook/react-vite@7.6.17(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.2)(vite@5.1.6)': + '@storybook/react-vite@7.6.17(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(rollup@4.13.0)(typescript@5.4.2)(vite@5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.0(typescript@5.4.2)(vite@5.1.6) - '@rollup/pluginutils': 5.1.0 - '@storybook/builder-vite': 7.6.17(typescript@5.4.2)(vite@5.1.6) - '@storybook/react': 7.6.17(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.2) - '@vitejs/plugin-react': 3.1.0(vite@5.1.6) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.0(typescript@5.4.2)(vite@5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3)) + '@rollup/pluginutils': 5.1.0(rollup@4.13.0) + '@storybook/builder-vite': 7.6.17(encoding@0.1.13)(typescript@5.4.2)(vite@5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3)) + '@storybook/react': 7.6.17(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.4.2) + '@vitejs/plugin-react': 3.1.0(vite@5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3)) magic-string: 0.30.8 react: 18.2.0 react-docgen: 7.0.3 react-dom: 18.2.0(react@18.2.0) - vite: 5.1.6(@types/node@18.19.24)(sass@1.71.1) + vite: 5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3) transitivePeerDependencies: - '@preact/preset-vite' - encoding @@ -15663,14 +15720,14 @@ snapshots: - typescript - vite-plugin-glimmerx - '@storybook/react@7.6.17(react-dom@18.2.0)(react@18.2.0)(typescript@5.4.2)': + '@storybook/react@7.6.17(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.4.2)': dependencies: '@storybook/client-logger': 7.6.17 '@storybook/core-client': 7.6.17 - '@storybook/docs-tools': 7.6.17 + '@storybook/docs-tools': 7.6.17(encoding@0.1.13) '@storybook/global': 5.0.0 '@storybook/preview-api': 7.6.17 - '@storybook/react-dom-shim': 7.6.17(react-dom@18.2.0)(react@18.2.0) + '@storybook/react-dom-shim': 7.6.17(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@storybook/types': 7.6.17 '@types/escodegen': 0.0.6 '@types/estree': 0.0.51 @@ -15684,11 +15741,12 @@ snapshots: prop-types: 15.8.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-element-to-jsx-string: 15.0.0(react-dom@18.2.0)(react@18.2.0) + react-element-to-jsx-string: 15.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) ts-dedent: 2.2.0 type-fest: 2.19.0 - typescript: 5.4.2 util-deprecate: 1.0.2 + optionalDependencies: + typescript: 5.4.2 transitivePeerDependencies: - encoding - supports-color @@ -15699,10 +15757,10 @@ snapshots: memoizerific: 1.11.3 qs: 6.12.0 - '@storybook/telemetry@7.6.17': + '@storybook/telemetry@7.6.17(encoding@0.1.13)': dependencies: '@storybook/client-logger': 7.6.17 - '@storybook/core-common': 7.6.17 + '@storybook/core-common': 7.6.17(encoding@0.1.13) '@storybook/csf-tools': 7.6.17 chalk: 4.1.2 detect-package-manager: 2.0.1 @@ -15721,7 +15779,7 @@ snapshots: '@testing-library/user-event': 13.5.0(@testing-library/dom@8.20.1) ts-dedent: 2.2.0 - '@storybook/theming@7.6.17(react-dom@18.2.0)(react@18.2.0)': + '@storybook/theming@7.6.17(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) '@storybook/client-logger': 7.6.17 @@ -15872,7 +15930,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0)': + '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.4.2))': dependencies: '@babel/core': 7.24.0 '@svgr/babel-preset': 8.1.0(@babel/core@7.24.0) @@ -15982,20 +16040,20 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.4.2(@types/jest@27.5.2)(jest@23.6.0)': + '@testing-library/jest-dom@6.4.2(@types/jest@27.5.2)': dependencies: '@adobe/css-tools': 4.3.3 '@babel/runtime': 7.24.0 - '@types/jest': 27.5.2 aria-query: 5.3.0 chalk: 3.0.0 css.escape: 1.5.1 dom-accessibility-api: 0.6.3 - jest: 23.6.0 lodash: 4.17.21 redent: 3.0.0 + optionalDependencies: + '@types/jest': 27.5.2 - '@testing-library/react@14.2.1(react-dom@18.2.0)(react@18.2.0)': + '@testing-library/react@14.2.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.24.0 '@testing-library/dom': 9.3.4 @@ -16012,11 +16070,11 @@ snapshots: dependencies: '@testing-library/dom': 10.0.0 - '@toast-ui/react-image-editor@3.15.2(react@18.2.0)': + '@toast-ui/react-image-editor@3.15.2(encoding@0.1.13)(react@18.2.0)': dependencies: - fabric: 4.6.0 + fabric: 4.6.0(encoding@0.1.13) react: 18.2.0 - tui-image-editor: 3.15.3 + tui-image-editor: 3.15.3(encoding@0.1.13) transitivePeerDependencies: - bufferutil - encoding @@ -16375,9 +16433,9 @@ snapshots: dependencies: '@types/react': 18.2.65 - '@types/react-select@5.0.1(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0)': + '@types/react-select@5.0.1(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - react-select: 5.8.0(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + react-select: 5.8.0(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) transitivePeerDependencies: - '@types/react' - react @@ -16430,9 +16488,9 @@ snapshots: '@types/stylis@4.2.0': {} - '@types/testing-library__jest-dom@6.0.0(@types/jest@27.5.2)(jest@23.6.0)': + '@types/testing-library__jest-dom@6.0.0(@types/jest@27.5.2)': dependencies: - '@testing-library/jest-dom': 6.4.2(@types/jest@27.5.2)(jest@23.6.0) + '@testing-library/jest-dom': 6.4.2(@types/jest@27.5.2) transitivePeerDependencies: - '@jest/globals' - '@types/bun' @@ -16470,7 +16528,7 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.4.2)': + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint@8.57.0)(typescript@5.4.2)': dependencies: '@eslint-community/regexpp': 4.10.0 '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.2) @@ -16485,6 +16543,7 @@ snapshots: natural-compare: 1.4.0 semver: 7.6.0 ts-api-utils: 1.3.0(typescript@5.4.2) + optionalDependencies: typescript: 5.4.2 transitivePeerDependencies: - supports-color @@ -16497,6 +16556,7 @@ snapshots: '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.3.4(supports-color@5.5.0) eslint: 8.57.0 + optionalDependencies: typescript: 5.4.2 transitivePeerDependencies: - supports-color @@ -16513,6 +16573,7 @@ snapshots: debug: 4.3.4(supports-color@5.5.0) eslint: 8.57.0 ts-api-utils: 1.3.0(typescript@5.4.2) + optionalDependencies: typescript: 5.4.2 transitivePeerDependencies: - supports-color @@ -16529,6 +16590,7 @@ snapshots: minimatch: 9.0.3 semver: 7.6.0 ts-api-utils: 1.3.0(typescript@5.4.2) + optionalDependencies: typescript: 5.4.2 transitivePeerDependencies: - supports-color @@ -16577,21 +16639,21 @@ snapshots: global: 4.4.0 is-function: 1.0.2 - '@vitejs/plugin-react-swc@3.6.0(vite@5.1.6)': + '@vitejs/plugin-react-swc@3.6.0(vite@5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3))': dependencies: '@swc/core': 1.4.7 - vite: 5.1.6(@types/node@18.19.24)(sass@1.71.1) + vite: 5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3) transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@3.1.0(vite@5.1.6)': + '@vitejs/plugin-react@3.1.0(vite@5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3))': dependencies: '@babel/core': 7.24.0 '@babel/plugin-transform-react-jsx-self': 7.23.3(@babel/core@7.24.0) '@babel/plugin-transform-react-jsx-source': 7.23.3(@babel/core@7.24.0) magic-string: 0.27.0 react-refresh: 0.14.0 - vite: 5.1.6(@types/node@18.19.24)(sass@1.71.1) + vite: 5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3) transitivePeerDependencies: - supports-color @@ -16637,8 +16699,9 @@ snapshots: minimatch: 9.0.3 muggle-string: 0.3.1 path-browserify: 1.0.1 - typescript: 5.4.2 vue-template-compiler: 2.7.16 + optionalDependencies: + typescript: 5.4.2 '@vue/shared@3.4.21': {} @@ -17071,7 +17134,7 @@ snapshots: ansicolors@0.3.2: {} any-observable@0.3.0(rxjs@6.6.7): - dependencies: + optionalDependencies: rxjs: 6.6.7 anymatch@2.0.0: @@ -17706,9 +17769,9 @@ snapshots: canonicalize@1.0.8: {} - canvas@2.11.2: + canvas@2.11.2(encoding@0.1.13): dependencies: - '@mapbox/node-pre-gyp': 1.0.11 + '@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13) nan: 2.19.0 simple-get: 3.1.1 transitivePeerDependencies: @@ -18125,6 +18188,7 @@ snapshots: js-yaml: 4.1.0 parse-json: 5.2.0 path-type: 4.0.0 + optionalDependencies: typescript: 5.4.2 country-locale-map@1.9.4: @@ -18162,9 +18226,9 @@ snapshots: dependencies: cross-spawn: 7.0.3 - cross-fetch@4.0.0: + cross-fetch@4.0.0(encoding@0.1.13): dependencies: - node-fetch: 2.7.0 + node-fetch: 2.7.0(encoding@0.1.13) transitivePeerDependencies: - encoding @@ -18348,6 +18412,7 @@ snapshots: debug@4.3.4(supports-color@5.5.0): dependencies: ms: 2.1.2 + optionalDependencies: supports-color: 5.5.0 decamelize-keys@1.1.1: @@ -18916,11 +18981,12 @@ snapshots: dependencies: eslint: 8.57.0 - eslint-plugin-unused-imports@3.1.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint@8.57.0): + eslint-plugin-unused-imports@3.1.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint@8.57.0)(typescript@5.4.2))(eslint@8.57.0): dependencies: - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.4.2) eslint: 8.57.0 eslint-rule-composer: 0.3.0 + optionalDependencies: + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2))(eslint@8.57.0)(typescript@5.4.2) eslint-rule-composer@0.3.0: {} @@ -19206,10 +19272,10 @@ snapshots: eyes@0.1.8: {} - fabric@4.6.0: + fabric@4.6.0(encoding@0.1.13): optionalDependencies: - canvas: 2.11.2 - jsdom: 15.2.1(canvas@2.11.2) + canvas: 2.11.2(encoding@0.1.13) + jsdom: 15.2.1(canvas@2.11.2(encoding@0.1.13)) transitivePeerDependencies: - bufferutil - encoding @@ -19466,11 +19532,11 @@ snapshots: dependencies: fetch-blob: 3.2.0 - formik-material-ui@4.0.0-alpha.2(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@mui/material@5.15.13)(formik@2.4.5)(react@18.2.0)(tiny-warning@1.0.3): + formik-material-ui@4.0.0-alpha.2(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@mui/material@5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(formik@2.4.5(react@18.2.0))(react@18.2.0)(tiny-warning@1.0.3): dependencies: '@emotion/react': 11.11.4(@types/react@18.2.65)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.65)(react@18.2.0) - '@mui/material': 5.15.13(@emotion/react@11.11.4)(@emotion/styled@11.11.0)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0) + '@mui/material': 5.15.13(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.4(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react@18.2.0))(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) formik: 2.4.5(react@18.2.0) react: 18.2.0 tiny-warning: 1.0.3 @@ -20175,9 +20241,9 @@ snapshots: dependencies: '@babel/runtime': 7.24.4 - i18next-http-backend@2.5.2: + i18next-http-backend@2.5.2(encoding@0.1.13): dependencies: - cross-fetch: 4.0.0 + cross-fetch: 4.0.0(encoding@0.1.13) transitivePeerDependencies: - encoding @@ -20635,9 +20701,9 @@ snapshots: node-fetch: 1.7.3 whatwg-fetch: 3.6.20 - isomorphic-fetch@3.0.0: + isomorphic-fetch@3.0.0(encoding@0.1.13): dependencies: - node-fetch: 2.7.0 + node-fetch: 2.7.0(encoding@0.1.13) whatwg-fetch: 3.6.20 transitivePeerDependencies: - encoding @@ -20841,7 +20907,7 @@ snapshots: - bufferutil - utf-8-validate - jest-environment-jsdom@29.7.0: + jest-environment-jsdom@29.7.0(canvas@2.11.2(encoding@0.1.13)): dependencies: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 @@ -20850,7 +20916,9 @@ snapshots: '@types/node': 18.19.24 jest-mock: 29.7.0 jest-util: 29.7.0 - jsdom: 20.0.3 + jsdom: 20.0.3(canvas@2.11.2(encoding@0.1.13)) + optionalDependencies: + canvas: 2.11.2(encoding@0.1.13) transitivePeerDependencies: - bufferutil - supports-color @@ -21121,7 +21189,7 @@ snapshots: jsbn@0.1.1: {} - jscodeshift@0.15.2(@babel/preset-env@7.24.0): + jscodeshift@0.15.2(@babel/preset-env@7.24.0(@babel/core@7.24.0)): dependencies: '@babel/core': 7.24.0 '@babel/parser': 7.24.0 @@ -21130,7 +21198,6 @@ snapshots: '@babel/plugin-transform-nullish-coalescing-operator': 7.23.4(@babel/core@7.24.0) '@babel/plugin-transform-optional-chaining': 7.23.4(@babel/core@7.24.0) '@babel/plugin-transform-private-methods': 7.23.3(@babel/core@7.24.0) - '@babel/preset-env': 7.24.0(@babel/core@7.24.0) '@babel/preset-flow': 7.24.0(@babel/core@7.24.0) '@babel/preset-typescript': 7.23.3(@babel/core@7.24.0) '@babel/register': 7.23.7(@babel/core@7.24.0) @@ -21144,6 +21211,8 @@ snapshots: recast: 0.23.6 temp: 0.8.4 write-file-atomic: 2.4.3 + optionalDependencies: + '@babel/preset-env': 7.24.0(@babel/core@7.24.0) transitivePeerDependencies: - supports-color @@ -21179,13 +21248,12 @@ snapshots: - bufferutil - utf-8-validate - jsdom@15.2.1(canvas@2.11.2): + jsdom@15.2.1(canvas@2.11.2(encoding@0.1.13)): dependencies: abab: 2.0.6 acorn: 7.4.1 acorn-globals: 4.3.4 array-equal: 1.0.2 - canvas: 2.11.2 cssom: 0.4.4 cssstyle: 2.3.0 data-urls: 1.1.0 @@ -21208,12 +21276,14 @@ snapshots: whatwg-url: 7.1.0 ws: 7.5.9 xml-name-validator: 3.0.0 + optionalDependencies: + canvas: 2.11.2(encoding@0.1.13) transitivePeerDependencies: - bufferutil - utf-8-validate optional: true - jsdom@20.0.3: + jsdom@20.0.3(canvas@2.11.2(encoding@0.1.13)): dependencies: abab: 2.0.6 acorn: 8.11.3 @@ -21241,6 +21311,8 @@ snapshots: whatwg-url: 11.0.0 ws: 8.16.0 xml-name-validator: 4.0.0 + optionalDependencies: + canvas: 2.11.2(encoding@0.1.13) transitivePeerDependencies: - bufferutil - supports-color @@ -21290,9 +21362,9 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 - jsonld@8.3.2: + jsonld@8.3.2(web-streams-polyfill@3.3.3): dependencies: - '@digitalbazaar/http-client': 3.4.1 + '@digitalbazaar/http-client': 3.4.1(web-streams-polyfill@3.3.3) canonicalize: 1.0.8 lru-cache: 6.0.0 rdf-canonize: 3.4.0 @@ -21346,11 +21418,13 @@ snapshots: kuler@2.0.0: {} - ky-universal@0.11.0(ky@0.33.3): + ky-universal@0.11.0(ky@0.33.3)(web-streams-polyfill@3.3.3): dependencies: abort-controller: 3.0.0 ky: 0.33.3 node-fetch: 3.3.2 + optionalDependencies: + web-streams-polyfill: 3.3.3 ky@0.33.3: {} @@ -21824,7 +21898,7 @@ snapshots: merge-descriptors@1.0.1: {} merge-refs@1.2.2(@types/react@18.2.65): - dependencies: + optionalDependencies: '@types/react': 18.2.65 merge-stream@1.0.1: @@ -22066,9 +22140,11 @@ snapshots: encoding: 0.1.13 is-stream: 1.1.0 - node-fetch@2.7.0: + node-fetch@2.7.0(encoding@0.1.13): dependencies: whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 node-fetch@3.3.2: dependencies: @@ -22483,9 +22559,9 @@ snapshots: ieee754: 1.2.1 resolve-protobuf-schema: 2.1.0 - pdfjs-dist@3.11.174: + pdfjs-dist@3.11.174(encoding@0.1.13): optionalDependencies: - canvas: 2.11.2 + canvas: 2.11.2(encoding@0.1.13) path2d-polyfill: 2.0.1 transitivePeerDependencies: - encoding @@ -22789,7 +22865,12 @@ snapshots: dependencies: setimmediate: 1.0.5 - react-ace@10.1.0(react-dom@18.2.0)(react@18.2.0): + re-resizable@6.9.17(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + react-ace@10.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: ace-builds: 1.32.7 diff-match-patch: 1.0.5 @@ -22799,7 +22880,7 @@ snapshots: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-bootstrap@1.6.8(react-dom@18.2.0)(react@18.2.0): + react-bootstrap@1.6.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.24.0 '@restart/context': 2.1.4(react@18.2.0) @@ -22816,12 +22897,12 @@ snapshots: prop-types-extra: 1.1.1(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-overlays: 5.2.1(react-dom@18.2.0)(react@18.2.0) - react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + react-overlays: 5.2.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) uncontrollable: 7.2.1(react@18.2.0) warning: 4.0.3 - react-burger-menu@2.9.2(react-dom@18.2.0)(react@18.2.0): + react-burger-menu@2.9.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: browserify-optional: 1.0.1 classnames: 2.5.1 @@ -22836,14 +22917,14 @@ snapshots: '@babel/runtime': 7.24.4 react: 18.2.0 - react-colorful@5.6.1(react-dom@18.2.0)(react@18.2.0): + react-colorful@5.6.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-compare-image@3.4.0(react-dom@18.2.0)(react@18.2.0): + react-compare-image@3.4.0(canvas@2.11.2(encoding@0.1.13))(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: - jest-environment-jsdom: 29.7.0 + jest-environment-jsdom: 29.7.0(canvas@2.11.2(encoding@0.1.13)) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) transitivePeerDependencies: @@ -22856,16 +22937,18 @@ snapshots: dependencies: dnd-core: 16.0.1 - react-dnd@16.0.1(@types/node@18.19.24)(@types/react@18.2.65)(react@18.2.0): + react-dnd@16.0.1(@types/hoist-non-react-statics@3.3.5)(@types/node@18.19.24)(@types/react@18.2.65)(react@18.2.0): dependencies: '@react-dnd/invariant': 4.0.2 '@react-dnd/shallowequal': 4.0.2 - '@types/node': 18.19.24 - '@types/react': 18.2.65 dnd-core: 16.0.1 fast-deep-equal: 3.1.3 hoist-non-react-statics: 3.3.2 react: 18.2.0 + optionalDependencies: + '@types/hoist-non-react-statics': 3.3.5 + '@types/node': 18.19.24 + '@types/react': 18.2.65 react-docgen-typescript@2.2.2(typescript@5.4.2): dependencies: @@ -22899,7 +22982,7 @@ snapshots: prop-types: 15.8.1 react: 18.2.0 - react-element-to-jsx-string@15.0.0(react-dom@18.2.0)(react@18.2.0): + react-element-to-jsx-string@15.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@base2/pretty-print-object': 1.0.1 is-plain-object: 5.0.0 @@ -22914,17 +22997,17 @@ snapshots: react-focus-lock@2.11.2(@types/react@18.2.65)(react@18.2.0): dependencies: '@babel/runtime': 7.24.4 - '@types/react': 18.2.65 focus-lock: 1.3.4 prop-types: 15.8.1 react: 18.2.0 react-clientside-effect: 1.2.6(react@18.2.0) use-callback-ref: 1.3.1(@types/react@18.2.65)(react@18.2.0) use-sidecar: 1.1.2(@types/react@18.2.65)(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.65 react-focus-on@3.9.2(@types/react@18.2.65)(react@18.2.0): dependencies: - '@types/react': 18.2.65 aria-hidden: 1.2.3 react: 18.2.0 react-focus-lock: 2.11.2(@types/react@18.2.65)(react@18.2.0) @@ -22933,6 +23016,8 @@ snapshots: tslib: 2.6.2 use-callback-ref: 1.3.1(@types/react@18.2.65)(react@18.2.0) use-sidecar: 1.1.2(@types/react@18.2.65)(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.65 react-full-screen@0.2.5(prop-types@15.8.1)(react@18.2.0): dependencies: @@ -22941,17 +23026,17 @@ snapshots: prop-types: 15.8.1 react: 18.2.0 - react-google-font-loader@1.1.0(react-dom@18.2.0)(react@18.2.0): + react-google-font-loader@1.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: prop-types: 15.8.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-grid-gallery@0.5.6(react-dom@18.2.0)(react@18.2.0): + react-grid-gallery@0.5.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: prop-types: 15.8.1 react: 18.2.0 - react-images: 0.5.19(react-dom@18.2.0)(react@18.2.0) + react-images: 0.5.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0) transitivePeerDependencies: - react-dom @@ -22959,20 +23044,22 @@ snapshots: dependencies: react: 18.2.0 - react-i18next@14.1.0(i18next@23.11.4)(react-dom@18.2.0)(react@18.2.0): + react-i18next@14.1.0(i18next@23.11.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.24.4 html-parse-stringify: 3.0.1 i18next: 23.11.4 react: 18.2.0 + optionalDependencies: react-dom: 18.2.0(react@18.2.0) - react-i18next@14.1.1(i18next@23.11.4)(react-dom@18.2.0)(react@18.2.0): + react-i18next@14.1.1(i18next@23.11.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.24.4 html-parse-stringify: 3.0.1 i18next: 23.11.4 react: 18.2.0 + optionalDependencies: react-dom: 18.2.0(react@18.2.0) react-image-gallery@0.9.1(react@18.2.0): @@ -22984,23 +23071,23 @@ snapshots: react-swipeable: 5.5.1(react@18.2.0) resize-observer-polyfill: 1.5.1 - react-image-magnifiers@1.4.0(react-dom@18.2.0)(react@18.2.0): + react-image-magnifiers@1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: prop-types: 15.8.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-input-position: 1.3.2(react-dom@18.2.0)(react@18.2.0) + react-input-position: 1.3.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - react-images@0.5.19(react-dom@18.2.0)(react@18.2.0): + react-images@0.5.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: aphrodite: 0.5.0 prop-types: 15.8.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-scrolllock: 2.0.7(react-dom@18.2.0)(react@18.2.0) - react-transition-group: 2.9.0(react-dom@18.2.0)(react@18.2.0) + react-scrolllock: 2.0.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react-transition-group: 2.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - react-images@1.1.7(@types/react@18.2.65)(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0): + react-images@1.1.7(@types/react@18.2.65)(prop-types@15.8.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: a11y-focus-store: 1.0.0 cross-env: 7.0.3 @@ -23012,12 +23099,12 @@ snapshots: react-dom: 18.2.0(react@18.2.0) react-focus-on: 3.9.2(@types/react@18.2.65)(react@18.2.0) react-full-screen: 0.2.5(prop-types@15.8.1)(react@18.2.0) - react-transition-group: 2.9.0(react-dom@18.2.0)(react@18.2.0) - react-view-pager: 0.6.0(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0) + react-transition-group: 2.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react-view-pager: 0.6.0(prop-types@15.8.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) transitivePeerDependencies: - '@types/react' - react-input-position@1.3.2(react-dom@18.2.0)(react@18.2.0): + react-input-position@1.3.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: prop-types: 15.8.1 react: 18.2.0 @@ -23031,9 +23118,9 @@ snapshots: react-is@18.2.0: {} - react-leaflet@4.2.1(leaflet@1.9.4)(react-dom@18.2.0)(react@18.2.0): + react-leaflet@4.2.1(leaflet@1.9.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: - '@react-leaflet/core': 2.1.0(leaflet@1.9.4)(react-dom@18.2.0)(react@18.2.0) + '@react-leaflet/core': 2.1.0(leaflet@1.9.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) leaflet: 1.9.4 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -23051,7 +23138,7 @@ snapshots: dependencies: react: 18.2.0 - react-overlays@5.2.1(react-dom@18.2.0)(react@18.2.0): + react-overlays@5.2.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.24.0 '@popperjs/core': 2.11.8 @@ -23064,20 +23151,21 @@ snapshots: uncontrollable: 7.2.1(react@18.2.0) warning: 4.0.3 - react-pdf@7.7.1(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0): + react-pdf@7.7.1(@types/react@18.2.65)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: - '@types/react': 18.2.65 clsx: 2.1.0 dequal: 2.0.3 make-cancellable-promise: 1.3.2 make-event-props: 1.6.2 merge-refs: 1.2.2(@types/react@18.2.65) - pdfjs-dist: 3.11.174 + pdfjs-dist: 3.11.174(encoding@0.1.13) prop-types: 15.8.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) tiny-invariant: 1.3.3 warning: 4.0.3 + optionalDependencies: + '@types/react': 18.2.65 transitivePeerDependencies: - encoding - supports-color @@ -23091,7 +23179,7 @@ snapshots: react: 18.2.0 react-fast-compare: 3.2.2 - react-prop-toggle@1.0.2(react-dom@18.2.0)(react@18.2.0): + react-prop-toggle@1.0.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -23100,42 +23188,46 @@ snapshots: react-redux@9.1.0(@types/react@18.2.65)(react@18.2.0)(redux@4.2.1): dependencies: - '@types/react': 18.2.65 '@types/use-sync-external-store': 0.0.3 react: 18.2.0 - redux: 4.2.1 use-sync-external-store: 1.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.65 + redux: 4.2.1 react-refresh@0.14.0: {} react-remove-scroll-bar@2.3.5(@types/react@18.2.65)(react@18.2.0): dependencies: - '@types/react': 18.2.65 react: 18.2.0 react-style-singleton: 2.2.1(@types/react@18.2.65)(react@18.2.0) tslib: 2.6.2 + optionalDependencies: + '@types/react': 18.2.65 react-remove-scroll@2.5.5(@types/react@18.2.65)(react@18.2.0): dependencies: - '@types/react': 18.2.65 react: 18.2.0 react-remove-scroll-bar: 2.3.5(@types/react@18.2.65)(react@18.2.0) react-style-singleton: 2.2.1(@types/react@18.2.65)(react@18.2.0) tslib: 2.6.2 use-callback-ref: 1.3.1(@types/react@18.2.65)(react@18.2.0) use-sidecar: 1.1.2(@types/react@18.2.65)(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.65 react-remove-scroll@2.5.7(@types/react@18.2.65)(react@18.2.0): dependencies: - '@types/react': 18.2.65 react: 18.2.0 react-remove-scroll-bar: 2.3.5(@types/react@18.2.65)(react@18.2.0) react-style-singleton: 2.2.1(@types/react@18.2.65)(react@18.2.0) tslib: 2.6.2 use-callback-ref: 1.3.1(@types/react@18.2.65)(react@18.2.0) use-sidecar: 1.1.2(@types/react@18.2.65)(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.65 - react-router-dom@6.18.0(react-dom@18.2.0)(react@18.2.0): + react-router-dom@6.18.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@remix-run/router': 1.11.0 react: 18.2.0 @@ -23147,15 +23239,15 @@ snapshots: '@remix-run/router': 1.11.0 react: 18.2.0 - react-scrolllock@2.0.7(react-dom@18.2.0)(react@18.2.0): + react-scrolllock@2.0.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: exenv: 1.2.2 - react-prop-toggle: 1.0.2(react-dom@18.2.0)(react@18.2.0) + react-prop-toggle: 1.0.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) transitivePeerDependencies: - react - react-dom - react-select@5.8.0(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0): + react-select@5.8.0(@types/react@18.2.65)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.24.0 '@emotion/cache': 11.11.0 @@ -23166,7 +23258,7 @@ snapshots: prop-types: 15.8.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.65)(react@18.2.0) transitivePeerDependencies: - '@types/react' @@ -23175,24 +23267,25 @@ snapshots: react-style-singleton@2.2.1(@types/react@18.2.65)(react@18.2.0): dependencies: - '@types/react': 18.2.65 get-nonce: 1.0.1 invariant: 2.2.4 react: 18.2.0 tslib: 2.6.2 + optionalDependencies: + '@types/react': 18.2.65 react-swipeable@5.5.1(react@18.2.0): dependencies: prop-types: 15.8.1 react: 18.2.0 - react-toastify@9.1.3(react-dom@18.2.0)(react@18.2.0): + react-toastify@9.1.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: clsx: 1.2.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-transition-group@2.9.0(react-dom@18.2.0)(react@18.2.0): + react-transition-group@2.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: dom-helpers: 3.4.0 loose-envify: 1.4.0 @@ -23201,7 +23294,7 @@ snapshots: react-dom: 18.2.0(react@18.2.0) react-lifecycles-compat: 3.0.4 - react-transition-group@4.4.5(react-dom@18.2.0)(react@18.2.0): + react-transition-group@4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.24.0 dom-helpers: 5.2.1 @@ -23210,7 +23303,7 @@ snapshots: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-view-pager@0.6.0(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0): + react-view-pager@0.6.0(prop-types@15.8.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: animation-bus: 0.2.0 get-prefix: 1.0.0 @@ -23222,7 +23315,7 @@ snapshots: resize-observer-polyfill: 1.5.0 tabbable: 1.1.2 - react-virtualized@9.22.5(react-dom@18.2.0)(react@18.2.0): + react-virtualized@9.22.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.24.0 clsx: 1.2.1 @@ -23237,14 +23330,14 @@ snapshots: dependencies: loose-envify: 1.4.0 - reactflow@11.10.4(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0): + reactflow@11.10.4(@types/react@18.2.65)(immer@10.0.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: - '@reactflow/background': 11.3.9(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/controls': 11.2.9(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/core': 11.10.4(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/minimap': 11.7.9(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/node-resizer': 2.2.9(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/node-toolbar': 1.3.9(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/background': 11.3.9(@types/react@18.2.65)(immer@10.0.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@reactflow/controls': 11.2.9(@types/react@18.2.65)(immer@10.0.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.65)(immer@10.0.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@reactflow/minimap': 11.7.9(@types/react@18.2.65)(immer@10.0.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@reactflow/node-resizer': 2.2.9(@types/react@18.2.65)(immer@10.0.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@reactflow/node-toolbar': 1.3.9(@types/react@18.2.65)(immer@10.0.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) transitivePeerDependencies: @@ -23353,7 +23446,7 @@ snapshots: dependencies: esprima: 4.0.1 - redux-form@8.3.10(react-redux@9.1.0)(react@18.2.0)(redux@4.2.1): + redux-form@8.3.10(immutable@4.3.5)(react-redux@9.1.0(@types/react@18.2.65)(react@18.2.0)(redux@4.2.1))(react@18.2.0)(redux@4.2.1): dependencies: '@babel/runtime': 7.24.0 es6-error: 4.1.1 @@ -23366,6 +23459,8 @@ snapshots: react-is: 16.13.1 react-redux: 9.1.0(@types/react@18.2.65)(react@18.2.0)(redux@4.2.1) redux: 4.2.1 + optionalDependencies: + immutable: 4.3.5 redux-thunk@3.1.0(redux@5.0.1): dependencies: @@ -23754,18 +23849,20 @@ snapshots: sass-loader@13.3.3(sass@1.71.1)(webpack@5.91.0): dependencies: neo-async: 2.6.2 - sass: 1.71.1 webpack: 5.91.0 + optionalDependencies: + sass: 1.71.1 sass-loader@8.0.2(sass@1.71.1)(webpack@5.91.0): dependencies: clone-deep: 4.0.1 loader-utils: 1.4.2 neo-async: 2.6.2 - sass: 1.71.1 schema-utils: 2.7.1 semver: 6.3.1 webpack: 5.91.0 + optionalDependencies: + sass: 1.71.1 sass@1.71.1: dependencies: @@ -23804,13 +23901,13 @@ snapshots: self-closing-tags@1.0.1: {} - semantic-release@15.14.0(@octokit/core@6.1.2): + semantic-release@15.14.0(@octokit/core@6.1.2)(encoding@0.1.13): dependencies: - '@semantic-release/commit-analyzer': 6.3.3(semantic-release@15.14.0) + '@semantic-release/commit-analyzer': 6.3.3(semantic-release@15.14.0(@octokit/core@6.1.2)(encoding@0.1.13)) '@semantic-release/error': 2.2.0 - '@semantic-release/github': 5.5.8(@octokit/core@6.1.2)(semantic-release@15.14.0) - '@semantic-release/npm': 5.3.5(semantic-release@15.14.0) - '@semantic-release/release-notes-generator': 7.3.5(semantic-release@15.14.0) + '@semantic-release/github': 5.5.8(@octokit/core@6.1.2)(encoding@0.1.13)(semantic-release@15.14.0(@octokit/core@6.1.2)(encoding@0.1.13)) + '@semantic-release/npm': 5.3.5(semantic-release@15.14.0(@octokit/core@6.1.2)(encoding@0.1.13)) + '@semantic-release/release-notes-generator': 7.3.5(semantic-release@15.14.0(@octokit/core@6.1.2)(encoding@0.1.13)) aggregate-error: 3.1.0 cosmiconfig: 6.0.0 debug: 4.3.4(supports-color@5.5.0) @@ -24164,9 +24261,9 @@ snapshots: store2@2.14.3: {} - storybook@7.6.17: + storybook@7.6.17(encoding@0.1.13): dependencies: - '@storybook/cli': 7.6.17 + '@storybook/cli': 7.6.17(encoding@0.1.13) transitivePeerDependencies: - bufferutil - encoding @@ -24323,7 +24420,7 @@ snapshots: dependencies: inline-style-parser: 0.1.1 - styled-components@6.1.8(react-dom@18.2.0)(react@18.2.0): + styled-components@6.1.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@emotion/is-prop-valid': 1.2.1 '@emotion/unitless': 0.8.0 @@ -24636,7 +24733,7 @@ snapshots: semver: 5.7.2 yargs-parser: 10.1.0 - ts-node@10.9.2(@types/node@10.17.60)(typescript@5.4.2): + ts-node@10.9.2(@swc/core@1.4.7)(@types/node@10.17.60)(typescript@5.4.2): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.9 @@ -24653,8 +24750,10 @@ snapshots: typescript: 5.4.2 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.4.7 - ts-node@10.9.2(@types/node@18.19.24)(typescript@5.4.2): + ts-node@10.9.2(@swc/core@1.4.7)(@types/node@18.19.24)(typescript@5.4.2): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.9 @@ -24671,6 +24770,8 @@ snapshots: typescript: 5.4.2 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.4.7 ts-node@7.0.1: dependencies: @@ -24695,14 +24796,14 @@ snapshots: tslint-config-prettier@1.18.0: {} - tslint-config-standard@8.0.1(tslint@5.20.1)(typescript@5.4.2): + tslint-config-standard@8.0.1(tslint@5.20.1(typescript@5.4.2))(typescript@5.4.2): dependencies: - tslint-eslint-rules: 5.4.0(tslint@5.20.1)(typescript@5.4.2) + tslint-eslint-rules: 5.4.0(tslint@5.20.1(typescript@5.4.2))(typescript@5.4.2) transitivePeerDependencies: - tslint - typescript - tslint-eslint-rules@5.4.0(tslint@5.20.1)(typescript@5.4.2): + tslint-eslint-rules@5.4.0(tslint@5.20.1(typescript@5.4.2))(typescript@5.4.2): dependencies: doctrine: 0.7.2 tslib: 1.9.0 @@ -24741,9 +24842,9 @@ snapshots: tui-color-picker@2.2.8: {} - tui-image-editor@3.15.3: + tui-image-editor@3.15.3(encoding@0.1.13): dependencies: - fabric: 4.6.0 + fabric: 4.6.0(encoding@0.1.13) tui-code-snippet: 2.3.3 tui-color-picker: 2.2.8 transitivePeerDependencies: @@ -25046,16 +25147,18 @@ snapshots: use-callback-ref@1.3.1(@types/react@18.2.65)(react@18.2.0): dependencies: - '@types/react': 18.2.65 react: 18.2.0 tslib: 2.6.2 + optionalDependencies: + '@types/react': 18.2.65 use-isomorphic-layout-effect@1.1.2(@types/react@18.2.65)(react@18.2.0): dependencies: - '@types/react': 18.2.65 react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.65 - use-resize-observer@9.1.0(react-dom@18.2.0)(react@18.2.0): + use-resize-observer@9.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@juggle/resize-observer': 3.4.0 react: 18.2.0 @@ -25063,10 +25166,11 @@ snapshots: use-sidecar@1.1.2(@types/react@18.2.65)(react@18.2.0): dependencies: - '@types/react': 18.2.65 detect-node-es: 1.1.0 react: 18.2.0 tslib: 2.6.2 + optionalDependencies: + '@types/react': 18.2.65 use-sync-external-store@1.2.0(react@18.2.0): dependencies: @@ -25182,71 +25286,78 @@ snapshots: replace-ext: 2.0.0 teex: 1.0.1 - vite-plugin-checker@0.6.4(eslint@8.57.0)(typescript@5.4.2)(vite@5.1.6): + vite-plugin-checker@0.6.4(eslint@8.57.0)(optionator@0.9.3)(typescript@5.4.2)(vite@5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3))(vue-tsc@1.8.27(typescript@5.4.2)): dependencies: '@babel/code-frame': 7.23.5 ansi-escapes: 4.3.2 chalk: 4.1.2 chokidar: 3.6.0 commander: 8.3.0 - eslint: 8.57.0 fast-glob: 3.3.2 fs-extra: 11.2.0 npm-run-path: 4.0.1 semver: 7.6.0 strip-ansi: 6.0.1 tiny-invariant: 1.3.3 - typescript: 5.4.2 - vite: 5.1.6(@types/node@18.19.24)(sass@1.71.1) + vite: 5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3) vscode-languageclient: 7.0.0 vscode-languageserver: 7.0.0 vscode-languageserver-textdocument: 1.0.11 vscode-uri: 3.0.8 + optionalDependencies: + eslint: 8.57.0 + optionator: 0.9.3 + typescript: 5.4.2 + vue-tsc: 1.8.27(typescript@5.4.2) - vite-plugin-dts@3.7.3(@types/node@18.19.24)(typescript@5.4.2)(vite@5.1.6): + vite-plugin-dts@3.7.3(@types/node@18.19.24)(rollup@4.13.0)(typescript@5.4.2)(vite@5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3)): dependencies: '@microsoft/api-extractor': 7.39.0(@types/node@18.19.24) - '@rollup/pluginutils': 5.1.0 + '@rollup/pluginutils': 5.1.0(rollup@4.13.0) '@vue/language-core': 1.8.27(typescript@5.4.2) debug: 4.3.4(supports-color@5.5.0) kolorist: 1.8.0 typescript: 5.4.2 - vite: 5.1.6(@types/node@18.19.24)(sass@1.71.1) vue-tsc: 1.8.27(typescript@5.4.2) + optionalDependencies: + vite: 5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3) transitivePeerDependencies: - '@types/node' - rollup - supports-color - vite-plugin-node@3.1.0(vite@5.1.6): + vite-plugin-node@3.1.0(@swc/core@1.4.7)(vite@5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3)): dependencies: '@rollup/pluginutils': 4.2.1 chalk: 4.1.2 debug: 4.3.4(supports-color@5.5.0) - vite: 5.1.6(@types/node@18.19.24)(sass@1.71.1) + vite: 5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3) + optionalDependencies: + '@swc/core': 1.4.7 transitivePeerDependencies: - supports-color - vite-plugin-svgr@4.2.0(typescript@5.4.2)(vite@5.1.6): + vite-plugin-svgr@4.2.0(rollup@4.13.0)(typescript@5.4.2)(vite@5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3)): dependencies: - '@rollup/pluginutils': 5.1.0 + '@rollup/pluginutils': 5.1.0(rollup@4.13.0) '@svgr/core': 8.1.0(typescript@5.4.2) - '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0) - vite: 5.1.6(@types/node@18.19.24)(sass@1.71.1) + '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.4.2)) + vite: 5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3) transitivePeerDependencies: - rollup - supports-color - typescript - vite@5.1.6(@types/node@18.19.24)(sass@1.71.1): + vite@5.1.6(@types/node@18.19.24)(sass@1.71.1)(terser@5.30.3): dependencies: - '@types/node': 18.19.24 esbuild: 0.19.12 postcss: 8.4.35 rollup: 4.13.0 - sass: 1.71.1 optionalDependencies: + '@types/node': 18.19.24 fsevents: 2.3.3 + sass: 1.71.1 + terser: 5.30.3 void-elements@3.1.0: {} @@ -25703,8 +25814,10 @@ snapshots: optionalDependencies: commander: 9.5.0 - zustand@4.5.2(@types/react@18.2.65)(react@18.2.0): + zustand@4.5.2(@types/react@18.2.65)(immer@10.0.4)(react@18.2.0): dependencies: + use-sync-external-store: 1.2.0(react@18.2.0) + optionalDependencies: '@types/react': 18.2.65 + immer: 10.0.4 react: 18.2.0 - use-sync-external-store: 1.2.0(react@18.2.0) diff --git a/uploader/api/src/Controller/Admin/CommitCrudController.php b/uploader/api/src/Controller/Admin/CommitCrudController.php index c5e5ea414..dcfcabb37 100644 --- a/uploader/api/src/Controller/Admin/CommitCrudController.php +++ b/uploader/api/src/Controller/Admin/CommitCrudController.php @@ -40,7 +40,6 @@ public function configureActions(Actions $actions): Actions return parent::configureActions($actions) ->remove(Crud::PAGE_INDEX, Action::NEW) - ->remove(Crud::PAGE_INDEX, Action::EDIT) ->add(Crud::PAGE_INDEX, $triggerAgainAction); } @@ -61,7 +60,8 @@ public function configureFields(string $pageName): iterable yield $this->userChoiceField->create('userId', 'User'); yield TextField::new('token') ->hideOnIndex(); - yield BooleanField::new('acknowledged')->renderAsSwitch(false); + yield BooleanField::new('acknowledged') + ->renderAsSwitch(false); yield TextField::new('notifyEmail') ->hideOnIndex(); yield IntegerField::new('totalSize') @@ -72,8 +72,10 @@ public function configureFields(string $pageName): iterable yield JsonField::new('options') ->hideOnIndex(); yield TextField::new('locale'); - yield DateTimeField::new('acknowledgedAt'); - yield DateTimeField::new('createdAt'); + yield DateTimeField::new('acknowledgedAt') + ->hideOnForm(); + yield DateTimeField::new('createdAt') + ->hideOnForm(); yield AssociationField::new('assets') ->hideOnForm(); }