From 4278d045c28ee8e6597e5210a5e4558e3a995000 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 16 Mar 2026 07:51:44 +0000 Subject: [PATCH] implements PR-020 --- ...apabilities-and-register-first-delivery.md | 101 ++++++++++++ docs/packer/pull-requests/README.md | 3 +- .../java/p/packer/PackerWorkspaceService.java | 4 + .../PackerAssetActionAvailabilityDTO.java | 20 +++ .../messages/GetAssetActionsRequest.java | 13 ++ .../messages/GetAssetActionsResult.java | 24 +++ .../packer/messages/RegisterAssetRequest.java | 13 ++ .../packer/messages/RegisterAssetResult.java | 26 +++ .../p/packer/messages/assets/AssetAction.java | 5 + .../src/main/java/p/packer/Packer.java | 8 +- .../models/PackerAssetActionAvailability.java | 20 +++ .../models/PackerRegisterAssetEvaluation.java | 20 +++ .../models/PackerResolvedAssetReference.java | 20 +++ .../FileSystemPackerWorkspaceService.java | 85 ++++++++++ .../PackerAssetActionReadService.java | 131 +++++++++++++++ .../services/PackerAssetDetailsService.java | 80 ++------- .../PackerAssetReferenceResolver.java | 88 ++++++++++ .../services/PackerIdentityAllocator.java | 24 +++ .../services/PackerReadMessageMapper.java | 16 ++ .../services/PackerWorkspaceFoundation.java | 8 + .../FileSystemPackerWorkspaceServiceTest.java | 153 +++++++++++++++++- .../PackerAssetDetailsServiceTest.java | 4 +- .../java/p/studio/utilities/i18n/I18n.java | 1 + .../assets/details/AssetDetailsControl.java | 114 ++++++++++++- .../assets/details/AssetDetailsUiSupport.java | 15 ++ .../messages/AssetWorkspaceAssetAction.java | 20 +++ .../messages/AssetWorkspaceAssetDetails.java | 2 + .../main/resources/i18n/messages.properties | 1 + .../resources/themes/default-prometeu.css | 4 + 29 files changed, 943 insertions(+), 80 deletions(-) create mode 100644 docs/packer/pull-requests/PR-20-asset-action-capabilities-and-register-first-delivery.md create mode 100644 prometeu-packer/prometeu-packer-api/src/main/java/p/packer/dtos/PackerAssetActionAvailabilityDTO.java create mode 100644 prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/GetAssetActionsRequest.java create mode 100644 prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/GetAssetActionsResult.java create mode 100644 prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/RegisterAssetRequest.java create mode 100644 prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/RegisterAssetResult.java create mode 100644 prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/assets/AssetAction.java create mode 100644 prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerAssetActionAvailability.java create mode 100644 prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerRegisterAssetEvaluation.java create mode 100644 prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerResolvedAssetReference.java create mode 100644 prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetActionReadService.java create mode 100644 prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetReferenceResolver.java create mode 100644 prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/AssetWorkspaceAssetAction.java diff --git a/docs/packer/pull-requests/PR-20-asset-action-capabilities-and-register-first-delivery.md b/docs/packer/pull-requests/PR-20-asset-action-capabilities-and-register-first-delivery.md new file mode 100644 index 00000000..e9807275 --- /dev/null +++ b/docs/packer/pull-requests/PR-20-asset-action-capabilities-and-register-first-delivery.md @@ -0,0 +1,101 @@ +# PR-20 Asset Action Capabilities and Register-First Delivery + +Domain Owner: `docs/packer` +Cross-Domain Impact: `docs/studio` + +## Briefing + +The current `Assets` workspace still decides too much locally about which asset actions should appear and when they should be allowed. + +That is the wrong long-term boundary. + +The packer already owns asset semantics, registration state, build participation, and write execution. It should also own the capability decision for asset actions, while Studio remains a consumer that renders the actions exposed by the service. + +This PR introduces a capability-based action contract driven by the packer, with `AssetAction.REGISTER` as the first delivered action end to end. + +## Objective + +Add a packer-owned asset action capability API and deliver the first action, `REGISTER`, through the full packer-to-Studio path. + +## Dependencies + +- [`./PR-15-snapshot-backed-asset-query-services.md`](./PR-15-snapshot-backed-asset-query-services.md) +- [`./PR-16-write-lane-command-completion-and-used-write-services.md`](./PR-16-write-lane-command-completion-and-used-write-services.md) +- [`./PR-17-studio-runtime-adapter-and-assets-workspace-consumption.md`](./PR-17-studio-runtime-adapter-and-assets-workspace-consumption.md) +- [`./PR-18-legacy-service-retirement-and-regression-hardening.md`](./PR-18-legacy-service-retirement-and-regression-hardening.md) +- [`./PR-19-api-surface-audit-model-separation-and-public-read-dtos.md`](./PR-19-api-surface-audit-model-separation-and-public-read-dtos.md) + +## Scope + +- define a public packer action contract for asset capabilities +- let the packer decide which actions are available for a given asset at a given moment +- let Studio render the action section from packer-provided capabilities instead of local semantic rules +- keep the capability contract stable even if the packer later moves to FSM or another internal decision model +- deliver `AssetAction.REGISTER` as the first supported action end to end +- keep the service-only wave focused on active functionality + +## Non-Goals + +- no FSM implementation +- no full action family rollout in the first delivery +- no doctor/build/reconcile action surfaces +- no speculative future actions without packer support +- no frontend-local capability engine + +## Execution Method + +1. Add a public action capability contract in `prometeu-packer-api`, including: + - `AssetAction` enum + - capability response shape for asset actions + - request/response messages for reading action capabilities +2. Model the response so the packer can expose at least: + - `action` + - `enabled` + - `visible` + - optional `reason` +3. Add a runtime-backed read path in `prometeu-packer-v1` that resolves action capabilities from the current asset state. +4. Keep the internal decision logic inside the packer implementation. Studio must not reconstruct capability semantics. +5. Add a write path for `AssetAction.REGISTER`. +6. Expose `REGISTER` only when the asset capability resolver says it is allowed. +7. Render the `Actions` section in Studio from the returned capabilities. +8. Execute `REGISTER` from Studio through the packer write path and refresh the asset workspace after completion. + +## Register-First Delivery Rules + +The first delivered action is: + +- `AssetAction.REGISTER` + +Expected initial capability behavior: + +- unregistered assets may expose `REGISTER` +- registered assets must not expose `REGISTER` +- Studio must only show the `Register` button when the packer returns that capability + +Other future actions such as `MOVE`, `DELETE`, `INCLUDE_IN_BUILD`, `EXCLUDE_FROM_BUILD`, `ENABLE_PRELOAD`, `DISABLE_PRELOAD`, and `CHANGE_CONTRACT` are intentionally deferred from code delivery in this PR unless needed to support the contract shape. + +## Acceptance Criteria + +- the packer API exposes an asset action capability contract +- Studio reads action capabilities from the packer instead of deciding them locally +- the first delivered action is `AssetAction.REGISTER` +- `REGISTER` is available only for unregistered assets +- `REGISTER` is not shown for registered assets +- Studio can trigger `REGISTER` through the packer service path and refresh the workspace after success +- the capability contract remains valid if packer internals later migrate to FSM or another orchestration model + +## Validation + +- packer unit tests for capability resolution +- packer tests for `REGISTER` execution +- Studio tests or smoke tests proving that action buttons are rendered from service-provided capabilities +- end-to-end validation for unregistered asset -> register -> refresh -> registered asset + +## Affected Artifacts + +- `prometeu-packer/prometeu-packer-api/src/main/java/p/packer/**` +- `prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/**` +- `prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/**` +- `prometeu-studio/src/main/java/p/studio/**` +- `prometeu-studio/src/test/java/p/studio/**` + diff --git a/docs/packer/pull-requests/README.md b/docs/packer/pull-requests/README.md index f63e0be1..c61dc740 100644 --- a/docs/packer/pull-requests/README.md +++ b/docs/packer/pull-requests/README.md @@ -79,6 +79,7 @@ The current production track for the standalone `prometeu-packer` project is: 17. [`PR-17-studio-runtime-adapter-and-assets-workspace-consumption.md`](./PR-17-studio-runtime-adapter-and-assets-workspace-consumption.md) 18. [`PR-18-legacy-service-retirement-and-regression-hardening.md`](./PR-18-legacy-service-retirement-and-regression-hardening.md) 19. [`PR-19-api-surface-audit-model-separation-and-public-read-dtos.md`](./PR-19-api-surface-audit-model-separation-and-public-read-dtos.md) +20. [`PR-20-asset-action-capabilities-and-register-first-delivery.md`](./PR-20-asset-action-capabilities-and-register-first-delivery.md) Current wave discipline from `PR-11` onward: @@ -89,4 +90,4 @@ Current wave discipline from `PR-11` onward: Recommended dependency chain: -`PR-01 -> PR-02 -> PR-03 -> PR-04 -> PR-05 -> PR-06 -> PR-07 -> PR-08 -> PR-09 -> PR-10 -> PR-11 -> PR-12 -> PR-13 -> PR-14 -> PR-15 -> PR-16 -> PR-17 -> PR-18 -> PR-19` +`PR-01 -> PR-02 -> PR-03 -> PR-04 -> PR-05 -> PR-06 -> PR-07 -> PR-08 -> PR-09 -> PR-10 -> PR-11 -> PR-12 -> PR-13 -> PR-14 -> PR-15 -> PR-16 -> PR-17 -> PR-18 -> PR-19 -> PR-20` diff --git a/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/PackerWorkspaceService.java b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/PackerWorkspaceService.java index 5c9332fa..62daa747 100644 --- a/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/PackerWorkspaceService.java +++ b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/PackerWorkspaceService.java @@ -9,5 +9,9 @@ public interface PackerWorkspaceService { GetAssetDetailsResult getAssetDetails(GetAssetDetailsRequest request); + GetAssetActionsResult getAssetActions(GetAssetActionsRequest request); + CreateAssetResult createAsset(CreateAssetRequest request); + + RegisterAssetResult registerAsset(RegisterAssetRequest request); } diff --git a/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/dtos/PackerAssetActionAvailabilityDTO.java b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/dtos/PackerAssetActionAvailabilityDTO.java new file mode 100644 index 00000000..ba74b152 --- /dev/null +++ b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/dtos/PackerAssetActionAvailabilityDTO.java @@ -0,0 +1,20 @@ +package p.packer.dtos; + +import p.packer.messages.assets.AssetAction; + +import java.util.Objects; + +public record PackerAssetActionAvailabilityDTO( + AssetAction action, + boolean enabled, + boolean visible, + String reason) { + + public PackerAssetActionAvailabilityDTO { + Objects.requireNonNull(action, "action"); + reason = reason == null ? null : reason.trim(); + if (reason != null && reason.isBlank()) { + reason = null; + } + } +} diff --git a/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/GetAssetActionsRequest.java b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/GetAssetActionsRequest.java new file mode 100644 index 00000000..a3913261 --- /dev/null +++ b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/GetAssetActionsRequest.java @@ -0,0 +1,13 @@ +package p.packer.messages; + +import java.util.Objects; + +public record GetAssetActionsRequest( + PackerProjectContext project, + AssetReference assetReference) { + + public GetAssetActionsRequest { + Objects.requireNonNull(project, "project"); + Objects.requireNonNull(assetReference, "assetReference"); + } +} diff --git a/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/GetAssetActionsResult.java b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/GetAssetActionsResult.java new file mode 100644 index 00000000..a87522b7 --- /dev/null +++ b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/GetAssetActionsResult.java @@ -0,0 +1,24 @@ +package p.packer.messages; + +import p.packer.dtos.PackerAssetActionAvailabilityDTO; +import p.packer.dtos.PackerDiagnosticDTO; + +import java.util.List; +import java.util.Objects; + +public record GetAssetActionsResult( + PackerOperationStatus status, + String summary, + List actions, + List diagnostics) { + + public GetAssetActionsResult { + Objects.requireNonNull(status, "status"); + summary = Objects.requireNonNull(summary, "summary").trim(); + actions = List.copyOf(Objects.requireNonNull(actions, "actions")); + diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics")); + if (summary.isBlank()) { + throw new IllegalArgumentException("summary must not be blank"); + } + } +} diff --git a/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/RegisterAssetRequest.java b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/RegisterAssetRequest.java new file mode 100644 index 00000000..2f4046c6 --- /dev/null +++ b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/RegisterAssetRequest.java @@ -0,0 +1,13 @@ +package p.packer.messages; + +import java.util.Objects; + +public record RegisterAssetRequest( + PackerProjectContext project, + AssetReference assetReference) { + + public RegisterAssetRequest { + Objects.requireNonNull(project, "project"); + Objects.requireNonNull(assetReference, "assetReference"); + } +} diff --git a/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/RegisterAssetResult.java b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/RegisterAssetResult.java new file mode 100644 index 00000000..660cd9c0 --- /dev/null +++ b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/RegisterAssetResult.java @@ -0,0 +1,26 @@ +package p.packer.messages; + +import java.nio.file.Path; +import java.util.Objects; + +public record RegisterAssetResult( + PackerOperationStatus status, + String summary, + AssetReference assetReference, + Path assetRoot, + Path manifestPath) { + + public RegisterAssetResult { + Objects.requireNonNull(status, "status"); + summary = Objects.requireNonNull(summary, "summary").trim(); + if (summary.isBlank()) { + throw new IllegalArgumentException("summary must not be blank"); + } + if (assetRoot != null) { + assetRoot = assetRoot.toAbsolutePath().normalize(); + } + if (manifestPath != null) { + manifestPath = manifestPath.toAbsolutePath().normalize(); + } + } +} diff --git a/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/assets/AssetAction.java b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/assets/AssetAction.java new file mode 100644 index 00000000..6fd38136 --- /dev/null +++ b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/assets/AssetAction.java @@ -0,0 +1,5 @@ +package p.packer.messages.assets; + +public enum AssetAction { + REGISTER +} diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/Packer.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/Packer.java index 3602ecde..e8c83d93 100644 --- a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/Packer.java +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/Packer.java @@ -26,11 +26,17 @@ public final class Packer implements Closeable { final PackerAssetDeclarationParser declarationParser = new PackerAssetDeclarationParser(); final PackerRuntimeRegistry runtimeRegistry = new PackerRuntimeRegistry( new PackerRuntimeLoader(workspaceFoundation, declarationParser)); - final PackerAssetDetailsService assetDetailsService = new PackerAssetDetailsService(runtimeRegistry); + final PackerAssetReferenceResolver assetReferenceResolver = new PackerAssetReferenceResolver(workspaceFoundation.lookup()); + final PackerAssetDetailsService assetDetailsService = new PackerAssetDetailsService(runtimeRegistry, assetReferenceResolver); + final PackerAssetActionReadService assetActionReadService = new PackerAssetActionReadService( + runtimeRegistry, + assetReferenceResolver, + workspaceFoundation.lookup()); final PackerProjectWriteCoordinator writeCoordinator = new PackerProjectWriteCoordinator(); return new Packer(new FileSystemPackerWorkspaceService( workspaceFoundation, assetDetailsService, + assetActionReadService, runtimeRegistry, writeCoordinator, resolvedEventSink), runtimeRegistry, writeCoordinator); diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerAssetActionAvailability.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerAssetActionAvailability.java new file mode 100644 index 00000000..ed9a7785 --- /dev/null +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerAssetActionAvailability.java @@ -0,0 +1,20 @@ +package p.packer.models; + +import p.packer.messages.assets.AssetAction; + +import java.util.Objects; + +public record PackerAssetActionAvailability( + AssetAction action, + boolean enabled, + boolean visible, + String reason) { + + public PackerAssetActionAvailability { + Objects.requireNonNull(action, "action"); + reason = reason == null ? null : reason.trim(); + if (reason != null && reason.isBlank()) { + reason = null; + } + } +} diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerRegisterAssetEvaluation.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerRegisterAssetEvaluation.java new file mode 100644 index 00000000..07996202 --- /dev/null +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerRegisterAssetEvaluation.java @@ -0,0 +1,20 @@ +package p.packer.models; + +import java.util.List; +import java.util.Objects; + +public record PackerRegisterAssetEvaluation( + PackerResolvedAssetReference resolved, + List diagnostics, + boolean canRegister, + String reason) { + + public PackerRegisterAssetEvaluation { + Objects.requireNonNull(resolved, "resolved"); + diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics")); + reason = reason == null ? null : reason.trim(); + if (reason != null && reason.isBlank()) { + reason = null; + } + } +} diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerResolvedAssetReference.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerResolvedAssetReference.java new file mode 100644 index 00000000..6c2a64bd --- /dev/null +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerResolvedAssetReference.java @@ -0,0 +1,20 @@ +package p.packer.models; + +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public record PackerResolvedAssetReference( + Path assetRoot, + Optional registryEntry, + Optional runtimeAsset, + List diagnostics) { + + public PackerResolvedAssetReference { + assetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize(); + registryEntry = Objects.requireNonNull(registryEntry, "registryEntry"); + runtimeAsset = Objects.requireNonNull(runtimeAsset, "runtimeAsset"); + diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics")); + } +} diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/FileSystemPackerWorkspaceService.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/FileSystemPackerWorkspaceService.java index 27946cd7..83658c4b 100644 --- a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/FileSystemPackerWorkspaceService.java +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/FileSystemPackerWorkspaceService.java @@ -5,6 +5,8 @@ import p.packer.messages.PackerOperationStatus; import p.packer.messages.PackerProjectContext; import p.packer.messages.assets.AssetFamilyCatalog; import p.packer.messages.AssetReference; +import p.packer.messages.GetAssetActionsRequest; +import p.packer.messages.GetAssetActionsResult; import p.packer.messages.assets.OutputFormatCatalog; import p.packer.messages.assets.PackerAssetState; import p.packer.messages.assets.PackerBuildParticipation; @@ -21,9 +23,12 @@ import p.packer.messages.InitWorkspaceRequest; import p.packer.messages.InitWorkspaceResult; import p.packer.messages.ListAssetsRequest; import p.packer.messages.ListAssetsResult; +import p.packer.messages.RegisterAssetRequest; +import p.packer.messages.RegisterAssetResult; import p.packer.PackerWorkspaceService; import p.packer.models.PackerAssetDeclarationParseResult; import p.packer.models.PackerAssetIdentity; +import p.packer.models.PackerRegisterAssetEvaluation; import p.packer.models.PackerAssetSummary; import p.packer.models.PackerDiagnostic; import p.packer.models.PackerRegistryEntry; @@ -41,6 +46,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe private static final ObjectMapper MAPPER = new ObjectMapper(); private final PackerWorkspaceFoundation workspaceFoundation; private final PackerAssetDetailsService detailsService; + private final PackerAssetActionReadService actionReadService; private final PackerRuntimeRegistry runtimeRegistry; private final PackerProjectWriteCoordinator writeCoordinator; private final PackerEventSink eventSink; @@ -48,11 +54,13 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe public FileSystemPackerWorkspaceService( PackerWorkspaceFoundation workspaceFoundation, PackerAssetDetailsService detailsService, + PackerAssetActionReadService actionReadService, PackerRuntimeRegistry runtimeRegistry, PackerProjectWriteCoordinator writeCoordinator, PackerEventSink eventSink) { this.workspaceFoundation = Objects.requireNonNull(workspaceFoundation, "workspaceFoundation"); this.detailsService = Objects.requireNonNull(detailsService, "detailsService"); + this.actionReadService = Objects.requireNonNull(actionReadService, "actionReadService"); this.runtimeRegistry = Objects.requireNonNull(runtimeRegistry, "runtimeRegistry"); this.writeCoordinator = Objects.requireNonNull(writeCoordinator, "writeCoordinator"); this.eventSink = Objects.requireNonNull(eventSink, "eventSink"); @@ -133,6 +141,11 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe return detailsService.getAssetDetails(request); } + @Override + public GetAssetActionsResult getAssetActions(GetAssetActionsRequest request) { + return actionReadService.getAssetActions(request); + } + @Override public CreateAssetResult createAsset(CreateAssetRequest request) { final CreateAssetRequest safeRequest = Objects.requireNonNull(request, "request"); @@ -142,6 +155,14 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe return writeCoordinator.execute(project, () -> createAssetInWriteLane(safeRequest, events)); } + @Override + public RegisterAssetResult registerAsset(RegisterAssetRequest request) { + final RegisterAssetRequest safeRequest = Objects.requireNonNull(request, "request"); + final PackerProjectContext project = safeRequest.project(); + final PackerOperationEventEmitter events = new PackerOperationEventEmitter(project, eventSink); + return writeCoordinator.execute(project, () -> registerAssetInWriteLane(safeRequest, events)); + } + private CreateAssetResult createAssetInWriteLane( CreateAssetRequest request, PackerOperationEventEmitter events) { @@ -216,6 +237,50 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe } } + private RegisterAssetResult registerAssetInWriteLane( + RegisterAssetRequest request, + PackerOperationEventEmitter events) { + final PackerProjectContext project = request.project(); + workspaceFoundation.initWorkspace(new InitWorkspaceRequest(project)); + final PackerRuntimeSnapshot snapshot = runtimeRegistry.getOrLoad(project).snapshot(); + final PackerRegisterAssetEvaluation evaluation = actionReadService.evaluateRegister(snapshot, project, request.assetReference()); + final Path assetRoot = evaluation.resolved().assetRoot(); + final Path manifestPath = assetRoot.resolve("asset.json"); + final String relativeAssetRoot = relativeAssetRoot(project, assetRoot); + if (!evaluation.canRegister()) { + final String summary = Objects.requireNonNullElse(evaluation.reason(), "Asset cannot be registered."); + return registerFailureResult(events, summary, assetRoot, manifestPath, List.of(relativeAssetRoot)); + } + + final var declaration = evaluation.resolved().runtimeAsset().orElseThrow().parsedDeclaration().declaration(); + try { + final PackerRegistryState registry = workspaceFoundation.loadRegistry(project); + final PackerRegistryEntry entry = workspaceFoundation.registerExistingAsset( + project, + registry, + assetRoot, + declaration.assetUuid()); + final PackerRegistryState updated = workspaceFoundation.appendAllocatedEntry(registry, entry); + workspaceFoundation.saveRegistry(project, updated); + runtimeRegistry.refresh(project); + final RegisterAssetResult result = new RegisterAssetResult( + PackerOperationStatus.SUCCESS, + "Asset registered: " + relativeAssetRoot, + AssetReference.forAssetId(entry.assetId()), + assetRoot, + manifestPath); + events.emit(PackerEventKind.ACTION_APPLIED, result.summary(), List.of(relativeAssetRoot)); + return result; + } catch (RuntimeException exception) { + return registerFailureResult( + events, + "Unable to register asset: " + exception.getMessage(), + assetRoot, + manifestPath, + List.of(relativeAssetRoot)); + } + } + private CreateAssetResult failureResult( PackerOperationEventEmitter events, String summary, @@ -232,6 +297,22 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe return result; } + private RegisterAssetResult registerFailureResult( + PackerOperationEventEmitter events, + String summary, + Path assetRoot, + Path manifestPath, + List affectedAssets) { + final RegisterAssetResult result = new RegisterAssetResult( + PackerOperationStatus.FAILED, + summary, + null, + assetRoot, + manifestPath); + events.emit(PackerEventKind.ACTION_FAILED, result.summary(), affectedAssets); + return result; + } + private void writeManifest(Path manifestPath, CreateAssetRequest request, String assetUuid) throws IOException { final Map manifest = new LinkedHashMap<>(); manifest.put("schema_version", 1); @@ -321,4 +402,8 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe .toString() .replace('\\', '/')); } + + private String relativeAssetRoot(PackerProjectContext project, Path assetRoot) { + return PackerWorkspacePaths.relativeAssetRoot(project, assetRoot).replace('\\', '/'); + } } diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetActionReadService.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetActionReadService.java new file mode 100644 index 00000000..29c58397 --- /dev/null +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetActionReadService.java @@ -0,0 +1,131 @@ +package p.packer.services; + +import p.packer.dtos.PackerAssetActionAvailabilityDTO; +import p.packer.messages.AssetReference; +import p.packer.messages.GetAssetActionsRequest; +import p.packer.messages.GetAssetActionsResult; +import p.packer.messages.PackerOperationStatus; +import p.packer.messages.PackerProjectContext; +import p.packer.messages.assets.AssetAction; +import p.packer.messages.diagnostics.PackerDiagnosticCategory; +import p.packer.messages.diagnostics.PackerDiagnosticSeverity; +import p.packer.models.PackerAssetActionAvailability; +import p.packer.models.PackerAssetDeclaration; +import p.packer.models.PackerAssetDeclarationParseResult; +import p.packer.models.PackerDiagnostic; +import p.packer.models.PackerRegisterAssetEvaluation; +import p.packer.models.PackerRegistryEntry; +import p.packer.models.PackerResolvedAssetReference; +import p.packer.models.PackerRuntimeAsset; +import p.packer.models.PackerRuntimeSnapshot; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public final class PackerAssetActionReadService { + private final PackerRuntimeRegistry runtimeRegistry; + private final PackerAssetReferenceResolver assetReferenceResolver; + private final PackerRegistryLookup registryLookup; + + public PackerAssetActionReadService( + PackerRuntimeRegistry runtimeRegistry, + PackerAssetReferenceResolver assetReferenceResolver, + PackerRegistryLookup registryLookup) { + this.runtimeRegistry = Objects.requireNonNull(runtimeRegistry, "runtimeRegistry"); + this.assetReferenceResolver = Objects.requireNonNull(assetReferenceResolver, "assetReferenceResolver"); + this.registryLookup = Objects.requireNonNull(registryLookup, "registryLookup"); + } + + public GetAssetActionsResult getAssetActions(GetAssetActionsRequest request) { + final PackerProjectContext project = Objects.requireNonNull(request, "request").project(); + final PackerRuntimeSnapshot snapshot = runtimeRegistry.getOrLoad(project).snapshot(); + final PackerRegisterAssetEvaluation registerEvaluation = evaluateRegister(snapshot, project, request.assetReference()); + final List actions = new ArrayList<>(); + if (registerEvaluation.resolved().registryEntry().isEmpty()) { + actions.add(new PackerAssetActionAvailability( + AssetAction.REGISTER, + registerEvaluation.canRegister(), + true, + registerEvaluation.reason())); + } + + final PackerOperationStatus status; + if (registerEvaluation.resolved().runtimeAsset().isEmpty()) { + status = PackerOperationStatus.FAILED; + } else if (registerEvaluation.diagnostics().stream().anyMatch(PackerDiagnostic::blocking)) { + status = PackerOperationStatus.PARTIAL; + } else { + status = PackerOperationStatus.SUCCESS; + } + return new GetAssetActionsResult( + status, + "Asset action capabilities resolved from runtime snapshot.", + PackerReadMessageMapper.toAssetActionAvailabilityDTOs(actions), + PackerReadMessageMapper.toDiagnosticDTOs(registerEvaluation.diagnostics())); + } + + public PackerRegisterAssetEvaluation evaluateRegister( + PackerRuntimeSnapshot snapshot, + PackerProjectContext project, + AssetReference assetReference) { + final PackerResolvedAssetReference resolved = assetReferenceResolver.resolve(project, snapshot, assetReference); + final List diagnostics = new ArrayList<>(resolved.diagnostics()); + if (resolved.registryEntry().isPresent()) { + return new PackerRegisterAssetEvaluation(resolved, diagnostics, false, "Asset is already registered."); + } + if (resolved.runtimeAsset().isEmpty()) { + diagnostics.add(new PackerDiagnostic( + PackerDiagnosticSeverity.ERROR, + PackerDiagnosticCategory.STRUCTURAL, + "asset.json was not found for the requested asset root.", + resolved.assetRoot().resolve("asset.json"), + true)); + return new PackerRegisterAssetEvaluation(resolved, diagnostics, false, "asset.json was not found for the requested asset root."); + } + + final PackerRuntimeAsset runtimeAsset = resolved.runtimeAsset().get(); + final PackerAssetDeclarationParseResult parsed = runtimeAsset.parsedDeclaration(); + diagnostics.addAll(parsed.diagnostics()); + if (!parsed.valid()) { + return new PackerRegisterAssetEvaluation(resolved, diagnostics, false, firstBlockingReason(diagnostics, "Asset declaration is invalid.")); + } + + final PackerAssetDeclaration declaration = parsed.declaration(); + final Optional sameUuidEntry = registryLookup.findByAssetUuid(snapshot.registry(), declaration.assetUuid()); + if (sameUuidEntry.isPresent()) { + final Path registeredRoot = PackerWorkspacePaths.assetRoot(project, sameUuidEntry.get().root()); + if (!registeredRoot.equals(resolved.assetRoot())) { + diagnostics.add(new PackerDiagnostic( + PackerDiagnosticSeverity.ERROR, + PackerDiagnosticCategory.STRUCTURAL, + "Field 'asset_uuid' is already registered to another asset root.", + runtimeAsset.manifestPath(), + true)); + return new PackerRegisterAssetEvaluation( + resolved, + diagnostics, + false, + "Asset UUID is already registered by another asset."); + } + } + + final Optional sameRootEntry = registryLookup.findByRoot(project, snapshot.registry(), resolved.assetRoot()); + if (sameRootEntry.isPresent()) { + return new PackerRegisterAssetEvaluation(resolved, diagnostics, false, "Asset root is already registered."); + } + + return new PackerRegisterAssetEvaluation(resolved, diagnostics, true, null); + } + + private String firstBlockingReason(List diagnostics, String fallback) { + return diagnostics.stream() + .filter(PackerDiagnostic::blocking) + .map(PackerDiagnostic::message) + .filter(message -> message != null && !message.isBlank()) + .findFirst() + .orElse(fallback); + } +} diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetDetailsService.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetDetailsService.java index 6542e1a2..87c5151f 100644 --- a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetDetailsService.java +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetDetailsService.java @@ -18,6 +18,7 @@ import p.packer.models.PackerAssetIdentity; import p.packer.models.PackerAssetSummary; import p.packer.models.PackerDiagnostic; import p.packer.models.PackerRegistryEntry; +import p.packer.models.PackerResolvedAssetReference; import p.packer.models.PackerRuntimeAsset; import p.packer.models.PackerRuntimeSnapshot; @@ -31,15 +32,19 @@ import java.util.Optional; public final class PackerAssetDetailsService { private final PackerRuntimeRegistry runtimeRegistry; + private final PackerAssetReferenceResolver assetReferenceResolver; - public PackerAssetDetailsService(PackerRuntimeRegistry runtimeRegistry) { + public PackerAssetDetailsService( + PackerRuntimeRegistry runtimeRegistry, + PackerAssetReferenceResolver assetReferenceResolver) { this.runtimeRegistry = Objects.requireNonNull(runtimeRegistry, "runtimeRegistry"); + this.assetReferenceResolver = Objects.requireNonNull(assetReferenceResolver, "assetReferenceResolver"); } public GetAssetDetailsResult getAssetDetails(GetAssetDetailsRequest request) { final PackerProjectContext project = Objects.requireNonNull(request, "request").project(); final PackerRuntimeSnapshot snapshot = runtimeRegistry.getOrLoad(project).snapshot(); - final ResolvedAssetReference resolved = resolveReference(project, snapshot, request.assetReference()); + final PackerResolvedAssetReference resolved = assetReferenceResolver.resolve(project, snapshot, request.assetReference()); final List diagnostics = new ArrayList<>(resolved.diagnostics()); if (resolved.runtimeAsset().isEmpty()) { @@ -100,7 +105,7 @@ public final class PackerAssetDetailsService { private GetAssetDetailsResult failureResult( PackerProjectContext project, AssetReference requestedReference, - ResolvedAssetReference resolved, + PackerResolvedAssetReference resolved, List diagnostics) { final PackerAssetState state = resolved.registryEntry().isPresent() ? PackerAssetState.REGISTERED @@ -157,44 +162,6 @@ public final class PackerAssetDetailsService { true)); } - private ResolvedAssetReference resolveReference(PackerProjectContext project, PackerRuntimeSnapshot snapshot, AssetReference assetReference) { - final PackerRegistryLookup lookup = new PackerRegistryLookup(); - final String reference = Objects.requireNonNull(assetReference, "assetReference").rawValue(); - final Optional byId = parseAssetId(reference).flatMap(assetId -> lookup.findByAssetId(snapshot.registry(), assetId)); - if (byId.isPresent()) { - final Path assetRoot = PackerWorkspacePaths.assetRoot(project, byId.get().root()); - return new ResolvedAssetReference(assetRoot, byId, findRuntimeAsset(snapshot, assetRoot), List.of()); - } - - final Optional byUuid = lookup.findByAssetUuid(snapshot.registry(), reference); - if (byUuid.isPresent()) { - final Path assetRoot = PackerWorkspacePaths.assetRoot(project, byUuid.get().root()); - return new ResolvedAssetReference(assetRoot, byUuid, findRuntimeAsset(snapshot, assetRoot), List.of()); - } - - final Path candidateRoot = PackerWorkspacePaths.assetRoot(project, reference); - final Optional runtimeAsset = findRuntimeAsset(snapshot, candidateRoot); - if (runtimeAsset.isPresent()) { - return new ResolvedAssetReference(candidateRoot, lookup.findByRoot(project, snapshot.registry(), candidateRoot), runtimeAsset, List.of()); - } - - final Optional registryEntry = lookup.findByRoot(project, snapshot.registry(), candidateRoot); - if (registryEntry.isPresent()) { - return new ResolvedAssetReference(candidateRoot, registryEntry, Optional.empty(), List.of()); - } - - return new ResolvedAssetReference( - candidateRoot, - Optional.empty(), - Optional.empty(), - List.of(new PackerDiagnostic( - PackerDiagnosticSeverity.ERROR, - PackerDiagnosticCategory.STRUCTURAL, - "Requested asset reference could not be resolved.", - candidateRoot, - true))); - } - private AssetReference canonicalReference( PackerProjectContext project, Path assetRoot, @@ -211,39 +178,10 @@ public final class PackerAssetDetailsService { private AssetReference canonicalReferenceOrRequested( PackerProjectContext project, AssetReference requestedReference, - ResolvedAssetReference resolved) { + PackerResolvedAssetReference resolved) { if (resolved.registryEntry().isPresent() || resolved.runtimeAsset().isPresent()) { return canonicalReference(project, resolved.assetRoot(), resolved.registryEntry()); } return requestedReference; } - - private Optional parseAssetId(String reference) { - try { - return Optional.of(Integer.parseInt(reference.trim())); - } catch (NumberFormatException ignored) { - return Optional.empty(); - } - } - - private Optional findRuntimeAsset(PackerRuntimeSnapshot snapshot, Path assetRoot) { - final Path normalizedRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize(); - return snapshot.assets().stream() - .filter(candidate -> candidate.assetRoot().equals(normalizedRoot)) - .findFirst(); - } - - private record ResolvedAssetReference( - Path assetRoot, - Optional registryEntry, - Optional runtimeAsset, - List diagnostics) { - - private ResolvedAssetReference { - assetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize(); - registryEntry = Objects.requireNonNull(registryEntry, "registryEntry"); - runtimeAsset = Objects.requireNonNull(runtimeAsset, "runtimeAsset"); - diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics")); - } - } } diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetReferenceResolver.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetReferenceResolver.java new file mode 100644 index 00000000..a2ac0d0f --- /dev/null +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetReferenceResolver.java @@ -0,0 +1,88 @@ +package p.packer.services; + +import p.packer.messages.AssetReference; +import p.packer.messages.PackerProjectContext; +import p.packer.messages.diagnostics.PackerDiagnosticCategory; +import p.packer.messages.diagnostics.PackerDiagnosticSeverity; +import p.packer.models.PackerDiagnostic; +import p.packer.models.PackerRegistryEntry; +import p.packer.models.PackerResolvedAssetReference; +import p.packer.models.PackerRuntimeAsset; +import p.packer.models.PackerRuntimeSnapshot; + +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public final class PackerAssetReferenceResolver { + private final PackerRegistryLookup lookup; + + public PackerAssetReferenceResolver() { + this(new PackerRegistryLookup()); + } + + public PackerAssetReferenceResolver(PackerRegistryLookup lookup) { + this.lookup = Objects.requireNonNull(lookup, "lookup"); + } + + public PackerResolvedAssetReference resolve( + PackerProjectContext project, + PackerRuntimeSnapshot snapshot, + AssetReference assetReference) { + final String reference = Objects.requireNonNull(assetReference, "assetReference").rawValue(); + final Optional byId = parseAssetId(reference) + .flatMap(assetId -> lookup.findByAssetId(snapshot.registry(), assetId)); + if (byId.isPresent()) { + final Path assetRoot = PackerWorkspacePaths.assetRoot(project, byId.get().root()); + return new PackerResolvedAssetReference(assetRoot, byId, findRuntimeAsset(snapshot, assetRoot), List.of()); + } + + final Optional byUuid = lookup.findByAssetUuid(snapshot.registry(), reference); + if (byUuid.isPresent()) { + final Path assetRoot = PackerWorkspacePaths.assetRoot(project, byUuid.get().root()); + return new PackerResolvedAssetReference(assetRoot, byUuid, findRuntimeAsset(snapshot, assetRoot), List.of()); + } + + final Path candidateRoot = PackerWorkspacePaths.assetRoot(project, reference); + final Optional runtimeAsset = findRuntimeAsset(snapshot, candidateRoot); + if (runtimeAsset.isPresent()) { + return new PackerResolvedAssetReference( + candidateRoot, + lookup.findByRoot(project, snapshot.registry(), candidateRoot), + runtimeAsset, + List.of()); + } + + final Optional registryEntry = lookup.findByRoot(project, snapshot.registry(), candidateRoot); + if (registryEntry.isPresent()) { + return new PackerResolvedAssetReference(candidateRoot, registryEntry, Optional.empty(), List.of()); + } + + return new PackerResolvedAssetReference( + candidateRoot, + Optional.empty(), + Optional.empty(), + List.of(new PackerDiagnostic( + PackerDiagnosticSeverity.ERROR, + PackerDiagnosticCategory.STRUCTURAL, + "Requested asset reference could not be resolved.", + candidateRoot, + true))); + } + + private Optional parseAssetId(String reference) { + try { + return Optional.of(Integer.parseInt(reference.trim())); + } catch (NumberFormatException ignored) { + return Optional.empty(); + } + } + + private Optional findRuntimeAsset(PackerRuntimeSnapshot snapshot, Path assetRoot) { + final Path normalizedRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize(); + return snapshot.assets().stream() + .filter(candidate -> candidate.assetRoot().equals(normalizedRoot)) + .findFirst(); + } +} diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerIdentityAllocator.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerIdentityAllocator.java index 65ba25dd..55e3c664 100644 --- a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerIdentityAllocator.java +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerIdentityAllocator.java @@ -26,6 +26,30 @@ public final class PackerIdentityAllocator { return new PackerRegistryEntry(state.nextAssetId(), UUID.randomUUID().toString(), relativeRoot); } + public PackerRegistryEntry registerExisting( + PackerProjectContext project, + PackerRegistryState state, + Path assetRoot, + String assetUuid) { + Objects.requireNonNull(project, "project"); + Objects.requireNonNull(state, "state"); + final Path normalizedAssetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize(); + final String relativeRoot = PackerWorkspacePaths.relativeAssetRoot(project, normalizedAssetRoot); + final String normalizedAssetUuid = Objects.requireNonNull(assetUuid, "assetUuid").trim(); + if (normalizedAssetUuid.isBlank()) { + throw new IllegalArgumentException("assetUuid must not be blank"); + } + final boolean alreadyRegisteredRoot = state.assets().stream().anyMatch(entry -> entry.root().equals(relativeRoot)); + if (alreadyRegisteredRoot) { + throw new p.packer.exceptions.PackerRegistryException("Asset root is already registered: " + relativeRoot); + } + final boolean alreadyRegisteredUuid = state.assets().stream().anyMatch(entry -> entry.assetUuid().equals(normalizedAssetUuid)); + if (alreadyRegisteredUuid) { + throw new p.packer.exceptions.PackerRegistryException("Asset UUID is already registered: " + normalizedAssetUuid); + } + return new PackerRegistryEntry(state.nextAssetId(), normalizedAssetUuid, relativeRoot); + } + public PackerRegistryState append(PackerRegistryState state, PackerRegistryEntry entry) { Objects.requireNonNull(state, "state"); Objects.requireNonNull(entry, "entry"); diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerReadMessageMapper.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerReadMessageMapper.java index 50271eaf..8f920dc9 100644 --- a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerReadMessageMapper.java +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerReadMessageMapper.java @@ -1,11 +1,13 @@ package p.packer.services; import p.packer.dtos.PackerAssetDetailsDTO; +import p.packer.dtos.PackerAssetActionAvailabilityDTO; import p.packer.dtos.PackerAssetIdentityDTO; import p.packer.dtos.PackerAssetSummaryDTO; import p.packer.dtos.PackerCodecConfigurationFieldDTO; import p.packer.dtos.PackerDiagnosticDTO; import p.packer.messages.assets.OutputCodecCatalog; +import p.packer.models.PackerAssetActionAvailability; import p.packer.models.PackerAssetDetails; import p.packer.models.PackerAssetIdentity; import p.packer.models.PackerAssetSummary; @@ -50,6 +52,11 @@ public final class PackerReadMessageMapper { return diagnostics.stream().map(PackerReadMessageMapper::toDiagnosticDTO).toList(); } + public static List toAssetActionAvailabilityDTOs( + List actions) { + return actions.stream().map(PackerReadMessageMapper::toAssetActionAvailabilityDTO).toList(); + } + private static PackerAssetIdentityDTO toAssetIdentityDTO(PackerAssetIdentity identity) { return new PackerAssetIdentityDTO( identity.assetId(), @@ -85,4 +92,13 @@ public final class PackerReadMessageMapper { diagnostic.evidencePath(), diagnostic.blocking()); } + + private static PackerAssetActionAvailabilityDTO toAssetActionAvailabilityDTO( + PackerAssetActionAvailability action) { + return new PackerAssetActionAvailabilityDTO( + action.action(), + action.enabled(), + action.visible(), + action.reason()); + } } diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerWorkspaceFoundation.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerWorkspaceFoundation.java index f75bd2b5..44b66be6 100644 --- a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerWorkspaceFoundation.java +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerWorkspaceFoundation.java @@ -58,6 +58,14 @@ public final class PackerWorkspaceFoundation { return identityAllocator.allocate(project, state, assetRoot); } + public PackerRegistryEntry registerExistingAsset( + PackerProjectContext project, + PackerRegistryState state, + java.nio.file.Path assetRoot, + String assetUuid) { + return identityAllocator.registerExisting(project, state, assetRoot, assetUuid); + } + public PackerRegistryState appendAllocatedEntry(PackerRegistryState state, PackerRegistryEntry entry) { return identityAllocator.append(state, entry); } diff --git a/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/FileSystemPackerWorkspaceServiceTest.java b/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/FileSystemPackerWorkspaceServiceTest.java index f9b5c7e8..e3d3ba9e 100644 --- a/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/FileSystemPackerWorkspaceServiceTest.java +++ b/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/FileSystemPackerWorkspaceServiceTest.java @@ -14,8 +14,12 @@ import p.packer.events.PackerEvent; import p.packer.events.PackerEventKind; import p.packer.messages.CreateAssetRequest; import p.packer.messages.CreateAssetResult; +import p.packer.messages.GetAssetActionsRequest; import p.packer.messages.GetAssetDetailsRequest; import p.packer.messages.ListAssetsRequest; +import p.packer.messages.RegisterAssetRequest; +import p.packer.messages.RegisterAssetResult; +import p.packer.messages.assets.AssetAction; import p.packer.testing.PackerFixtureLocator; import java.nio.file.Files; @@ -143,6 +147,143 @@ final class FileSystemPackerWorkspaceServiceTest { assertTrue(detailsResult.diagnostics().isEmpty()); } + @Test + void exposesRegisterActionForValidUnregisteredAsset() throws Exception { + final Path projectRoot = copyFixture("workspaces/orphan-valid", tempDir.resolve("orphan-actions")); + final FileSystemPackerWorkspaceService service = service(); + + final var result = service.getAssetActions(new GetAssetActionsRequest( + project(projectRoot), + AssetReference.forRelativeAssetRoot("orphans/ui_sounds"))); + + assertEquals(PackerOperationStatus.SUCCESS, result.status()); + assertEquals(1, result.actions().size()); + assertEquals(AssetAction.REGISTER, result.actions().getFirst().action()); + assertTrue(result.actions().getFirst().visible()); + assertTrue(result.actions().getFirst().enabled()); + assertNull(result.actions().getFirst().reason()); + } + + @Test + void hidesRegisterActionForRegisteredAsset() throws Exception { + final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("managed-actions")); + final FileSystemPackerWorkspaceService service = service(); + + final var result = service.getAssetActions(new GetAssetActionsRequest( + project(projectRoot), + AssetReference.forAssetId(1))); + + assertEquals(PackerOperationStatus.SUCCESS, result.status()); + assertTrue(result.actions().isEmpty()); + } + + @Test + void registersExistingUnregisteredAssetAndRefreshesRuntimeView() throws Exception { + final Path projectRoot = copyFixture("workspaces/orphan-valid", tempDir.resolve("register-orphan")); + final List events = new CopyOnWriteArrayList<>(); + final FileSystemPackerWorkspaceService service = service(events::add); + + final RegisterAssetResult registerResult = service.registerAsset(new RegisterAssetRequest( + project(projectRoot), + AssetReference.forRelativeAssetRoot("orphans/ui_sounds"))); + + assertEquals(PackerOperationStatus.SUCCESS, registerResult.status()); + assertNotNull(registerResult.assetReference()); + assertTrue(registerResult.assetReference().isAssetIdReference()); + + final var detailsResult = service.getAssetDetails(new GetAssetDetailsRequest( + project(projectRoot), + registerResult.assetReference())); + + assertEquals(PackerOperationStatus.SUCCESS, detailsResult.status()); + assertEquals(PackerAssetState.REGISTERED, detailsResult.details().summary().state()); + assertEquals("orphan-uuid-1", detailsResult.details().summary().identity().assetUuid()); + assertTrue(events.stream().anyMatch(event -> event.kind() == PackerEventKind.ACTION_APPLIED)); + } + + @Test + void blocksRegisterActionForInvalidDeclaration() throws Exception { + final Path projectRoot = copyFixture("workspaces/invalid-missing-fields", tempDir.resolve("invalid-actions")); + final FileSystemPackerWorkspaceService service = service(); + + final var result = service.getAssetActions(new GetAssetActionsRequest( + project(projectRoot), + AssetReference.forRelativeAssetRoot("bad"))); + + assertEquals(PackerOperationStatus.PARTIAL, result.status()); + assertEquals(1, result.actions().size()); + assertFalse(result.actions().getFirst().enabled()); + assertNotNull(result.actions().getFirst().reason()); + } + + @Test + void rejectsRegisterWhenAssetUuidIsAlreadyRegisteredElsewhere() throws Exception { + final Path projectRoot = tempDir.resolve("duplicate-uuid"); + Files.createDirectories(projectRoot.resolve("assets/registered/ui_atlas")); + Files.createDirectories(projectRoot.resolve("assets/orphans/ui_clone")); + Files.createDirectories(projectRoot.resolve("assets/.prometeu")); + Files.writeString(projectRoot.resolve("assets/registered/ui_atlas/asset.json"), """ + { + "schema_version": 1, + "asset_uuid": "shared-uuid", + "name": "ui_atlas", + "type": "IMAGE/bank", + "inputs": {}, + "output": { + "format": "TILES/indexed_v1", + "codec": "none" + }, + "preload": { + "enabled": true + } + } + """); + Files.writeString(projectRoot.resolve("assets/orphans/ui_clone/asset.json"), """ + { + "schema_version": 1, + "asset_uuid": "shared-uuid", + "name": "ui_clone", + "type": "IMAGE/bank", + "inputs": {}, + "output": { + "format": "TILES/indexed_v1", + "codec": "none" + }, + "preload": { + "enabled": false + } + } + """); + Files.writeString(projectRoot.resolve("assets/.prometeu/index.json"), """ + { + "schema_version": 1, + "next_asset_id": 2, + "assets": [ + { + "asset_id": 1, + "asset_uuid": "shared-uuid", + "root": "registered/ui_atlas", + "included_in_build": true + } + ] + } + """); + final FileSystemPackerWorkspaceService service = service(); + + final var actions = service.getAssetActions(new GetAssetActionsRequest( + project(projectRoot), + AssetReference.forRelativeAssetRoot("orphans/ui_clone"))); + assertEquals(1, actions.actions().size()); + assertFalse(actions.actions().getFirst().enabled()); + assertNotNull(actions.actions().getFirst().reason()); + + final RegisterAssetResult registerResult = service.registerAsset(new RegisterAssetRequest( + project(projectRoot), + AssetReference.forRelativeAssetRoot("orphans/ui_clone"))); + assertEquals(PackerOperationStatus.FAILED, registerResult.status()); + assertNull(registerResult.assetReference()); + } + @Test void rejectsUnsupportedFormatForSelectedFamily() { final Path projectRoot = tempDir.resolve("unsupported"); @@ -208,9 +349,17 @@ final class FileSystemPackerWorkspaceServiceTest { final var foundation = new p.packer.services.PackerWorkspaceFoundation(); final var parser = new p.packer.services.PackerAssetDeclarationParser(); final var runtimeRegistry = new p.packer.services.PackerRuntimeRegistry(new p.packer.services.PackerRuntimeLoader(foundation, parser)); - final var detailsService = new p.packer.services.PackerAssetDetailsService(runtimeRegistry); + final var resolver = new p.packer.services.PackerAssetReferenceResolver(foundation.lookup()); + final var detailsService = new p.packer.services.PackerAssetDetailsService(runtimeRegistry, resolver); + final var actionReadService = new p.packer.services.PackerAssetActionReadService(runtimeRegistry, resolver, foundation.lookup()); final var writeCoordinator = new p.packer.services.PackerProjectWriteCoordinator(); - return new FileSystemPackerWorkspaceService(foundation, detailsService, runtimeRegistry, writeCoordinator, eventSink); + return new FileSystemPackerWorkspaceService( + foundation, + detailsService, + actionReadService, + runtimeRegistry, + writeCoordinator, + eventSink); } private Path copyFixture(String relativePath, Path targetRoot) throws Exception { diff --git a/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/PackerAssetDetailsServiceTest.java b/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/PackerAssetDetailsServiceTest.java index ecf637ab..89263e5a 100644 --- a/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/PackerAssetDetailsServiceTest.java +++ b/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/PackerAssetDetailsServiceTest.java @@ -88,7 +88,9 @@ final class PackerAssetDetailsServiceTest { private PackerAssetDetailsService service() { final var foundation = new p.packer.services.PackerWorkspaceFoundation(); final var parser = new PackerAssetDeclarationParser(); - return new PackerAssetDetailsService(new PackerRuntimeRegistry(new PackerRuntimeLoader(foundation, parser))); + final var runtimeRegistry = new PackerRuntimeRegistry(new PackerRuntimeLoader(foundation, parser)); + final var resolver = new PackerAssetReferenceResolver(foundation.lookup()); + return new PackerAssetDetailsService(runtimeRegistry, resolver); } private Path copyFixture(String relativePath, Path targetRoot) throws Exception { diff --git a/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java b/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java index 1640a515..f32f27cb 100644 --- a/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java +++ b/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java @@ -100,6 +100,7 @@ public enum I18n { ASSETS_SECTION_INPUTS_PREVIEW("assets.section.inputsPreview"), ASSETS_SECTION_DIAGNOSTICS("assets.section.diagnostics"), ASSETS_SECTION_ACTIONS("assets.section.actions"), + ASSETS_ACTIONS_EMPTY("assets.actions.empty"), ASSETS_ACTION_REGISTER("assets.action.register"), ASSETS_ACTION_INCLUDE_IN_BUILD("assets.action.includeInBuild"), ASSETS_ACTION_EXCLUDE_FROM_BUILD("assets.action.excludeFromBuild"), diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsControl.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsControl.java index e7f7a4d1..029001b6 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsControl.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsControl.java @@ -1,20 +1,28 @@ package p.studio.workspaces.assets.details; import javafx.application.Platform; +import javafx.scene.Node; +import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import p.packer.messages.AssetReference; +import p.packer.dtos.PackerAssetActionAvailabilityDTO; import p.packer.dtos.PackerAssetDetailsDTO; import p.packer.messages.GetAssetDetailsRequest; +import p.packer.messages.GetAssetActionsRequest; +import p.packer.messages.RegisterAssetRequest; +import p.packer.messages.RegisterAssetResult; +import p.packer.messages.assets.AssetAction; import p.studio.Container; import p.studio.events.StudioWorkspaceEventBus; import p.studio.projects.ProjectReference; import p.studio.utilities.i18n.I18n; import p.studio.workspaces.assets.details.contract.AssetDetailsContractControl; import p.studio.workspaces.assets.details.summary.AssetDetailsSummaryControl; +import p.studio.workspaces.assets.messages.AssetWorkspaceAssetAction; import p.studio.workspaces.assets.messages.AssetWorkspaceAssetDetails; import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsStatus; import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState; @@ -46,6 +54,8 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware private AssetWorkspaceDetailsViewState viewState = AssetDetailsViewStateFactory.empty(); private boolean readyMounted; + private boolean actionRunning; + private String actionFeedbackMessage; public AssetDetailsControl( ProjectReference projectReference, @@ -109,12 +119,16 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware private void loadSelection(AssetReference assetReference) { final long generation = loadGeneration.incrementAndGet(); + actionRunning = false; + actionFeedbackMessage = null; publishViewState(AssetDetailsViewStateFactory.loading(assetReference)); Container.backgroundTasks().submit(() -> loadDetails(generation, assetReference)); } private void clearSelection() { loadGeneration.incrementAndGet(); + actionRunning = false; + actionFeedbackMessage = null; publishViewState(AssetDetailsViewStateFactory.empty()); } @@ -128,12 +142,15 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware private void loadDetails(long generation, AssetReference assetReference) { try { - final var response = Container.packer() - .workspaceService() + final var workspaceService = Container.packer().workspaceService(); + final var response = workspaceService .getAssetDetails(new GetAssetDetailsRequest( projectReference.toPackerProjectContext(), assetReference)); - final AssetWorkspaceAssetDetails details = mapDetails(response.details()); + final var actionsResponse = workspaceService.getAssetActions(new GetAssetActionsRequest( + projectReference.toPackerProjectContext(), + assetReference)); + final AssetWorkspaceAssetDetails details = mapDetails(response.details(), actionsResponse.actions()); Platform.runLater(() -> applyLoadedDetails(generation, assetReference, details)); } catch (RuntimeException exception) { Platform.runLater(() -> applyLoadFailure(generation, assetReference, exception)); @@ -175,18 +192,21 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware readyMounted = false; setDetailsTitle(null); setWorkspaceSummary(Container.i18n().text(I18n.ASSETS_SUMMARY_EMPTY)); + renderActions(); detailsContent.getChildren().setAll(AssetDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_NO_SELECTION))); } case LOADING -> { readyMounted = false; setDetailsTitle(null); setWorkspaceSummary(Container.i18n().text(I18n.ASSETS_SUMMARY_LOADING)); + renderActions(); detailsContent.getChildren().setAll(AssetDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_LOADING))); } case ERROR -> { readyMounted = false; setDetailsTitle(null); setWorkspaceSummary(Container.i18n().text(I18n.ASSETS_SUMMARY_ERROR)); + renderActions(); detailsContent.getChildren().setAll(AssetDetailsUiSupport.createSectionMessage( Objects.requireNonNullElse(viewState.detailsErrorMessage(), Container.i18n().text(I18n.ASSETS_DETAILS_EMPTY)))); } @@ -195,12 +215,14 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware readyMounted = false; setDetailsTitle(null); setWorkspaceSummary(null); + renderActions(); detailsContent.getChildren().setAll(AssetDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_NO_SELECTION))); return; } setDetailsTitle(viewState.selectedAssetDetails().summary().assetName()); setWorkspaceSummary(null); + renderActions(); if (!readyMounted) { readyMounted = true; detailsContent.getChildren().setAll(primarySectionsRow, contractControl); @@ -244,9 +266,93 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware workspaceSummaryLabel.setManaged(visible); } - private AssetWorkspaceAssetDetails mapDetails(PackerAssetDetailsDTO details) { + private void renderActions() { + actionsContent.getChildren().setAll(buildActionNodes()); + } + + private java.util.List buildActionNodes() { + final java.util.List nodes = new java.util.ArrayList<>(); + if (actionFeedbackMessage != null && !actionFeedbackMessage.isBlank()) { + nodes.add(AssetDetailsUiSupport.createSectionMessage(actionFeedbackMessage)); + } + if (viewState.selectedAssetDetails() == null) { + nodes.add(AssetDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_ACTIONS_EMPTY))); + return nodes; + } + final var visibleActions = viewState.selectedAssetDetails().actions().stream() + .filter(AssetWorkspaceAssetAction::visible) + .toList(); + if (visibleActions.isEmpty()) { + nodes.add(AssetDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_ACTIONS_EMPTY))); + return nodes; + } + for (AssetWorkspaceAssetAction action : visibleActions) { + final Button button = AssetDetailsUiSupport.createActionButton(AssetDetailsUiSupport.actionLabel(action.action())); + button.setDisable(actionRunning || !action.enabled()); + button.setOnAction(ignored -> executeAction(action)); + nodes.add(button); + if (action.reason() != null) { + nodes.add(AssetDetailsUiSupport.createSectionMessage(action.reason())); + } + } + return nodes; + } + + private void executeAction(AssetWorkspaceAssetAction action) { + if (actionRunning || viewState.selectedAssetReference() == null) { + return; + } + actionRunning = true; + actionFeedbackMessage = null; + renderActions(); + switch (action.action()) { + case REGISTER -> Container.backgroundTasks().submit(() -> registerSelectedAsset(viewState.selectedAssetReference())); + } + } + + private void registerSelectedAsset(AssetReference assetReference) { + try { + final RegisterAssetResult result = Container.packer().workspaceService().registerAsset(new RegisterAssetRequest( + projectReference.toPackerProjectContext(), + assetReference)); + Platform.runLater(() -> applyRegisterResult(result)); + } catch (RuntimeException exception) { + Platform.runLater(() -> applyRegisterFailure(exception)); + } + } + + private void applyRegisterResult(RegisterAssetResult result) { + actionRunning = false; + if (result.status() == p.packer.messages.PackerOperationStatus.SUCCESS && result.assetReference() != null) { + actionFeedbackMessage = null; + workspaceBus.publish(new StudioAssetsRefreshRequestedEvent(result.assetReference())); + workspaceBus.publish(new StudioAssetsWorkspaceSelectionRequestedEvent(result.assetReference())); + return; + } + actionFeedbackMessage = Objects.requireNonNullElse(result.summary(), "Unable to register asset."); + renderActions(); + } + + private void applyRegisterFailure(RuntimeException exception) { + actionRunning = false; + actionFeedbackMessage = exception.getMessage() == null || exception.getMessage().isBlank() + ? "Unable to register asset." + : exception.getMessage(); + renderActions(); + } + + private AssetWorkspaceAssetDetails mapDetails( + PackerAssetDetailsDTO details, + java.util.List actions) { return new AssetWorkspaceAssetDetails( AssetListPackerMappings.mapSummary(details.summary()), + actions.stream() + .map(action -> new AssetWorkspaceAssetAction( + action.action(), + action.enabled(), + action.visible(), + action.reason())) + .toList(), details.outputFormat(), details.outputCodec(), details.availableOutputCodecs(), diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsUiSupport.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsUiSupport.java index 36dd0b66..d73479a4 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsUiSupport.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsUiSupport.java @@ -2,10 +2,12 @@ package p.studio.workspaces.assets.details; import javafx.geometry.Pos; import javafx.scene.Node; +import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; +import p.packer.messages.assets.AssetAction; import p.packer.messages.assets.AssetFamilyCatalog; import p.studio.Container; import p.studio.projects.ProjectReference; @@ -58,6 +60,13 @@ public final class AssetDetailsUiSupport { return chip; } + public static Button createActionButton(String text) { + final Button button = new Button(text); + button.getStyleClass().addAll("studio-button", "studio-button-primary", "assets-details-action-button"); + button.setMaxWidth(Double.MAX_VALUE); + return button; + } + public static String registrationLabel(AssetWorkspaceAssetState state) { return switch (state) { case REGISTERED -> Container.i18n().text(I18n.ASSETS_VALUE_REGISTERED); @@ -96,6 +105,12 @@ public final class AssetDetailsUiSupport { }; } + public static String actionLabel(AssetAction action) { + return switch (action) { + case REGISTER -> Container.i18n().text(I18n.ASSETS_ACTION_REGISTER); + }; + } + public static String registrationChipTone(AssetWorkspaceAssetState state) { return switch (state) { case REGISTERED -> "assets-details-chip-registered"; diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/AssetWorkspaceAssetAction.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/AssetWorkspaceAssetAction.java new file mode 100644 index 00000000..0b4da1c6 --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/AssetWorkspaceAssetAction.java @@ -0,0 +1,20 @@ +package p.studio.workspaces.assets.messages; + +import p.packer.messages.assets.AssetAction; + +import java.util.Objects; + +public record AssetWorkspaceAssetAction( + AssetAction action, + boolean enabled, + boolean visible, + String reason) { + + public AssetWorkspaceAssetAction { + Objects.requireNonNull(action, "action"); + reason = reason == null ? null : reason.trim(); + if (reason != null && reason.isBlank()) { + reason = null; + } + } +} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/AssetWorkspaceAssetDetails.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/AssetWorkspaceAssetDetails.java index 96b2c519..e8867a0f 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/AssetWorkspaceAssetDetails.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/AssetWorkspaceAssetDetails.java @@ -10,6 +10,7 @@ import java.util.Objects; public record AssetWorkspaceAssetDetails( AssetWorkspaceAssetSummary summary, + List actions, String outputFormat, OutputCodecCatalog outputCodec, List availableOutputCodecs, @@ -18,6 +19,7 @@ public record AssetWorkspaceAssetDetails( public AssetWorkspaceAssetDetails { Objects.requireNonNull(summary, "summary"); + actions = List.copyOf(Objects.requireNonNull(actions, "actions")); outputFormat = Objects.requireNonNullElse(outputFormat, "unknown"); outputCodec = Objects.requireNonNullElse(outputCodec, OutputCodecCatalog.UNKNOWN); availableOutputCodecs = List.copyOf(Objects.requireNonNull(availableOutputCodecs, "availableOutputCodecs")); diff --git a/prometeu-studio/src/main/resources/i18n/messages.properties b/prometeu-studio/src/main/resources/i18n/messages.properties index ce3e5751..fbdf2e98 100644 --- a/prometeu-studio/src/main/resources/i18n/messages.properties +++ b/prometeu-studio/src/main/resources/i18n/messages.properties @@ -90,6 +90,7 @@ assets.subsection.codecConfiguration=Codec Configuration assets.section.inputsPreview=Inputs / Preview assets.section.diagnostics=Diagnostics assets.section.actions=Actions +assets.actions.empty=No actions available for this asset. assets.action.register=Register assets.action.includeInBuild=Include In Build assets.action.excludeFromBuild=Exclude From Build diff --git a/prometeu-studio/src/main/resources/themes/default-prometeu.css b/prometeu-studio/src/main/resources/themes/default-prometeu.css index 648940ea..fd1d88f1 100644 --- a/prometeu-studio/src/main/resources/themes/default-prometeu.css +++ b/prometeu-studio/src/main/resources/themes/default-prometeu.css @@ -547,6 +547,10 @@ -fx-spacing: 10; } +.assets-details-action-button { + -fx-max-width: Infinity; +} + .assets-details-section { -fx-background-color: #11151b; -fx-background-radius: 12;