From 2fa604e30843699c45374779772c5968f205947f Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Thu, 19 Mar 2026 00:56:46 +0000 Subject: [PATCH] implements PR-10f bank composition apply through packer and snapshot refresh --- .../java/p/packer/PackerWorkspaceService.java | 2 + .../messages/ApplyBankCompositionRequest.java | 16 ++++ .../ApplyBankCompositionResponse.java | 6 ++ .../models/PackerAssetArtifactSelection.java | 18 +++++ .../packer/models/PackerAssetDeclaration.java | 2 + .../FileSystemPackerWorkspaceService.java | 74 +++++++++++++++++++ .../PackerAssetDeclarationParser.java | 58 +++++++++++++++ .../services/PackerAssetDetailsService.java | 12 ++- .../FileSystemPackerWorkspaceServiceTest.java | 39 ++++++++++ .../PackerAssetDetailsServiceTest.java | 27 +++++++ .../AssetDetailsBankCompositionControl.java | 51 +++++++++---- 11 files changed, 288 insertions(+), 17 deletions(-) create mode 100644 prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/ApplyBankCompositionRequest.java create mode 100644 prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/ApplyBankCompositionResponse.java create mode 100644 prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerAssetArtifactSelection.java 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 d0bc1a90..ac0f5b31 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 @@ -20,4 +20,6 @@ public interface PackerWorkspaceService { DeleteAssetResult deleteAsset(DeleteAssetRequest request); UpdateAssetContractResponse updateAssetContract(UpdateAssetContractRequest request); + + ApplyBankCompositionResponse applyBankComposition(ApplyBankCompositionRequest request); } diff --git a/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/ApplyBankCompositionRequest.java b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/ApplyBankCompositionRequest.java new file mode 100644 index 00000000..fa570b45 --- /dev/null +++ b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/ApplyBankCompositionRequest.java @@ -0,0 +1,16 @@ +package p.packer.messages; + +import java.util.List; +import java.util.Objects; + +public record ApplyBankCompositionRequest( + PackerProjectContext project, + AssetReference assetReference, + List selectedFiles) { + + public ApplyBankCompositionRequest { + Objects.requireNonNull(project, "project"); + Objects.requireNonNull(assetReference, "assetReference"); + selectedFiles = List.copyOf(Objects.requireNonNull(selectedFiles, "selectedFiles")); + } +} diff --git a/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/ApplyBankCompositionResponse.java b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/ApplyBankCompositionResponse.java new file mode 100644 index 00000000..8ade2fc6 --- /dev/null +++ b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/ApplyBankCompositionResponse.java @@ -0,0 +1,6 @@ +package p.packer.messages; + +public record ApplyBankCompositionResponse( + boolean success, + String errorMessage) { +} diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerAssetArtifactSelection.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerAssetArtifactSelection.java new file mode 100644 index 00000000..5b0234e4 --- /dev/null +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerAssetArtifactSelection.java @@ -0,0 +1,18 @@ +package p.packer.models; + +import java.util.Objects; + +public record PackerAssetArtifactSelection( + String file, + int index) { + + public PackerAssetArtifactSelection { + file = Objects.requireNonNull(file, "file").trim(); + if (file.isBlank()) { + throw new IllegalArgumentException("file must not be blank"); + } + if (index < 0) { + throw new IllegalArgumentException("index must be non-negative"); + } + } +} diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerAssetDeclaration.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerAssetDeclaration.java index 95077471..b3d5e73a 100644 --- a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerAssetDeclaration.java +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerAssetDeclaration.java @@ -15,6 +15,7 @@ public record PackerAssetDeclaration( String name, AssetFamilyCatalog assetFamily, Map> inputsByRole, + List artifacts, OutputFormatCatalog outputFormat, OutputCodecCatalog outputCodec, Map outputMetadata, @@ -28,6 +29,7 @@ public record PackerAssetDeclaration( name = Objects.requireNonNull(name, "name").trim(); assetFamily = Objects.requireNonNull(assetFamily, "assetFamily"); inputsByRole = Map.copyOf(Objects.requireNonNull(inputsByRole, "inputsByRole")); + artifacts = List.copyOf(Objects.requireNonNull(artifacts, "artifacts")); outputFormat = Objects.requireNonNull(outputFormat, "outputFormat"); outputCodec = Objects.requireNonNull(outputCodec, "outputCodec"); outputMetadata = Map.copyOf(Objects.requireNonNull(outputMetadata, "outputMetadata")); 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 230329cf..ed698a5f 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 @@ -599,6 +599,13 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe return writeCoordinator.execute(project, () -> updateAssetContractInWriteLane(safeRequest)); } + @Override + public ApplyBankCompositionResponse applyBankComposition(final ApplyBankCompositionRequest request) { + final ApplyBankCompositionRequest safeRequest = Objects.requireNonNull(request, "request"); + final PackerProjectContext project = safeRequest.project(); + return writeCoordinator.execute(project, () -> applyBankCompositionInWriteLane(safeRequest)); + } + private UpdateAssetContractResponse updateAssetContractInWriteLane(UpdateAssetContractRequest request) { final PackerProjectContext project = request.project(); workspaceFoundation.initWorkspace(new InitWorkspaceRequest(project)); @@ -656,6 +663,52 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe } } + private ApplyBankCompositionResponse applyBankCompositionInWriteLane(ApplyBankCompositionRequest request) { + final PackerProjectContext project = request.project(); + workspaceFoundation.initWorkspace(new InitWorkspaceRequest(project)); + final PackerRuntimeSnapshot snapshot = runtimeRegistry.getOrLoad(project).snapshot(); + final PackerDeleteAssetEvaluation evaluation = actionReadService.evaluateDelete(snapshot, project, request.assetReference()); + if (!evaluation.canDelete()) { + return new ApplyBankCompositionResponse( + false, + Objects.requireNonNullElse(evaluation.reason(), "Asset bank composition cannot be updated.")); + } + + final Path assetRoot = evaluation.resolved().assetRoot(); + final Path manifestPath = assetRoot.resolve("asset.json"); + if (!Files.isRegularFile(manifestPath)) { + return new ApplyBankCompositionResponse(false, "asset.json was not found for the requested asset root."); + } + + final ObjectNode manifest; + try { + final JsonNode rawManifest = mapper.readTree(manifestPath.toFile()); + if (!(rawManifest instanceof ObjectNode objectNode)) { + return new ApplyBankCompositionResponse(false, "asset.json must contain a JSON object at the root."); + } + manifest = objectNode; + } catch (IOException exception) { + return new ApplyBankCompositionResponse(false, "Unable to read asset manifest: " + exception.getMessage()); + } + + try { + patchManifestArtifacts(manifest, request); + mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest); + final var runtime = runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterUpdateAssetContract( + currentSnapshot, + generation, + assetRoot, + manifestPath, + evaluation.resolved().registryEntry())); + saveRuntimeCache(project, runtime.snapshot()); + return new ApplyBankCompositionResponse(true, null); + } catch (IOException exception) { + return new ApplyBankCompositionResponse(false, "Unable to update asset bank composition: " + exception.getMessage()); + } catch (RuntimeException exception) { + return new ApplyBankCompositionResponse(false, "Unable to update runtime snapshot: " + exception.getMessage()); + } + } + private OutputFormatCatalog resolveManifestOutputFormat(ObjectNode manifest) { final JsonNode outputNode = manifest.get("output"); if (!(outputNode instanceof ObjectNode outputObject)) { @@ -701,6 +754,27 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe }); } + private void patchManifestArtifacts(ObjectNode manifest, ApplyBankCompositionRequest request) { + final ObjectNode inputsNode = mutableObject(manifest, "inputs"); + inputsNode.removeAll(); + + final var artifactsNode = manifest.putArray("artifacts"); + for (int index = 0; index < request.selectedFiles().size(); index += 1) { + final String file = Objects.requireNonNullElse(request.selectedFiles().get(index), "").trim(); + if (file.isBlank() || !isTrustedRelativePath(file)) { + throw new IllegalArgumentException("Selected artifact file paths must stay inside the asset root."); + } + final var artifactNode = artifactsNode.addObject(); + artifactNode.put("file", file); + artifactNode.put("index", index); + } + } + + private boolean isTrustedRelativePath(String value) { + final Path path = Path.of(value).normalize(); + return !path.isAbsolute() && !path.startsWith(".."); + } + private ObjectNode mutableObject(ObjectNode parent, String fieldName) { final JsonNode current = parent.get(fieldName); if (current instanceof ObjectNode objectNode) { diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetDeclarationParser.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetDeclarationParser.java index fa3ce882..9a337cbf 100644 --- a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetDeclarationParser.java +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetDeclarationParser.java @@ -7,6 +7,7 @@ import p.packer.messages.assets.OutputCodecCatalog; import p.packer.messages.assets.OutputFormatCatalog; import p.packer.messages.diagnostics.PackerDiagnosticCategory; import p.packer.messages.diagnostics.PackerDiagnosticSeverity; +import p.packer.models.PackerAssetArtifactSelection; import p.packer.models.PackerAssetDeclaration; import p.packer.models.PackerAssetDeclarationParseResult; import p.packer.models.PackerDiagnostic; @@ -45,6 +46,7 @@ public final class PackerAssetDeclarationParser { final var name = requiredText(root, "name", diagnostics, manifestPath); final var assetFamily = requiredAssetFamily(root, diagnostics, manifestPath); final var inputsByRole = requiredInputs(root.path("inputs"), diagnostics, manifestPath); + final var artifacts = optionalArtifacts(root.path("artifacts"), diagnostics, manifestPath); final var outputFormat = requiredOutputFormat(root.path("output"), diagnostics, manifestPath); final var outputCodec = requiredOutputCodec(root.path("output"), diagnostics, manifestPath); final var outputMetadata = optionalOutputMetadata(root.path("output"), diagnostics, manifestPath); @@ -70,6 +72,7 @@ public final class PackerAssetDeclarationParser { name, assetFamily, inputsByRole, + artifacts, outputFormat, outputCodec, outputMetadata, @@ -274,6 +277,61 @@ public final class PackerAssetDeclarationParser { return Map.copyOf(result); } + private List optionalArtifacts( + final JsonNode node, + final List diagnostics, + final Path manifestPath) { + if (node.isMissingNode() || node.isNull()) { + return List.of(); + } + if (!node.isArray()) { + diagnostics.add(new PackerDiagnostic( + PackerDiagnosticSeverity.ERROR, + PackerDiagnosticCategory.STRUCTURAL, + "Field 'artifacts' must be an array.", + manifestPath, + true)); + return List.of(); + } + + final List result = new ArrayList<>(); + for (JsonNode entry : node) { + if (!entry.isObject()) { + diagnostics.add(new PackerDiagnostic( + PackerDiagnosticSeverity.ERROR, + PackerDiagnosticCategory.STRUCTURAL, + "Each artifact entry must be an object with 'file' and 'index'.", + manifestPath, + true)); + continue; + } + final JsonNode fileNode = entry.path("file"); + final JsonNode indexNode = entry.path("index"); + if (!fileNode.isTextual() || fileNode.asText().isBlank() || !indexNode.isInt() || indexNode.asInt() < 0) { + diagnostics.add(new PackerDiagnostic( + PackerDiagnosticSeverity.ERROR, + PackerDiagnosticCategory.STRUCTURAL, + "Each artifact entry must define a non-blank 'file' and a non-negative integer 'index'.", + manifestPath, + true)); + continue; + } + final String relativePath = fileNode.asText().trim(); + if (!isTrustedRelativePath(relativePath)) { + diagnostics.add(new PackerDiagnostic( + PackerDiagnosticSeverity.ERROR, + PackerDiagnosticCategory.STRUCTURAL, + "Artifact file path must stay inside the asset root.", + manifestPath, + true)); + continue; + } + result.add(new PackerAssetArtifactSelection(relativePath, indexNode.asInt())); + } + result.sort(Comparator.comparingInt(PackerAssetArtifactSelection::index)); + return List.copyOf(result); + } + private boolean isTrustedRelativePath(final String value) { final Path path = Path.of(value).normalize(); return !path.isAbsolute() && !path.startsWith(".."); 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 16dbcfd3..19150bfe 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 @@ -150,7 +150,7 @@ public final class PackerAssetDetailsService { .map(this::toBankCompositionFile) .toList(); - final List selectedFiles = flattenSelectedInputPaths(declaration.inputsByRole()).stream() + final List selectedFiles = selectedPathsFor(declaration).stream() .map(path -> resolveSelectedBankFile(runtimeAsset.assetRoot(), path, walkFilesByPath)) .flatMap(Optional::stream) .toList(); @@ -161,9 +161,15 @@ public final class PackerAssetDetailsService { walkProjection.measuredBankSizeBytes()); } - private List flattenSelectedInputPaths(Map> inputsByRole) { + private List selectedPathsFor(PackerAssetDeclaration declaration) { + if (!declaration.artifacts().isEmpty()) { + return declaration.artifacts().stream() + .sorted(Comparator.comparingInt(PackerAssetArtifactSelection::index)) + .map(PackerAssetArtifactSelection::file) + .toList(); + } final List selected = new ArrayList<>(); - inputsByRole.values().forEach(selected::addAll); + declaration.inputsByRole().values().forEach(selected::addAll); return List.copyOf(selected); } 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 b2d745b1..e796bb3a 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 @@ -245,6 +245,45 @@ final class FileSystemPackerWorkspaceServiceTest { assertEquals(1, loader.loadCount()); } + @Test + void applyBankCompositionWritesArtifactsAndRefreshesSnapshotWithoutWholeProjectReload() throws Exception { + final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("apply-bank-composition")); + final Path assetRoot = projectRoot.resolve("assets/ui/atlas"); + Files.writeString(assetRoot.resolve("confirm.png"), "fixture"); + Files.writeString(assetRoot.resolve("cancel.png"), "fixture"); + final CountingLoader loader = countingLoader(); + final FileSystemPackerWorkspaceService service = service(ignored -> { }, loader); + + service.listAssets(new ListAssetsRequest(project(projectRoot))); + assertEquals(1, loader.loadCount()); + + final var applyResult = service.applyBankComposition(new ApplyBankCompositionRequest( + project(projectRoot), + AssetReference.forAssetId(1), + List.of("cancel.png", "confirm.png"))); + + assertTrue(applyResult.success()); + assertNull(applyResult.errorMessage()); + assertEquals(1, loader.loadCount()); + + final var manifest = MAPPER.readTree(assetRoot.resolve("asset.json").toFile()); + assertTrue(manifest.path("inputs").isObject()); + assertTrue(manifest.path("inputs").isEmpty()); + assertEquals("cancel.png", manifest.path("artifacts").get(0).path("file").asText()); + assertEquals(0, manifest.path("artifacts").get(0).path("index").asInt()); + assertEquals("confirm.png", manifest.path("artifacts").get(1).path("file").asText()); + assertEquals(1, manifest.path("artifacts").get(1).path("index").asInt()); + + final var detailsResult = service.getAssetDetails(new GetAssetDetailsRequest( + project(projectRoot), + AssetReference.forAssetId(1))); + assertEquals(PackerOperationStatus.SUCCESS, detailsResult.status()); + assertEquals( + List.of("cancel.png", "confirm.png"), + detailsResult.details().bankComposition().selectedFiles().stream().map(file -> file.path()).toList()); + assertEquals(1, loader.loadCount()); + } + @Test void returnsFailureWhenAssetManifestIsMissingOnContractUpdate() throws Exception { final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("update-contract-missing-manifest")); 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 37c0aa21..b27455bb 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 @@ -80,6 +80,33 @@ final class PackerAssetDetailsServiceTest { .allMatch(file -> !file.displayName().isBlank())); } + @Test + void prefersArtifactsSelectionOrderOverLegacyInputs() throws Exception { + final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("managed-artifacts-order")); + final Path assetRoot = projectRoot.resolve("assets/ui/atlas"); + final BufferedImage tile = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB); + ImageIO.write(tile, "png", assetRoot.resolve("a.png").toFile()); + ImageIO.write(tile, "png", assetRoot.resolve("b.png").toFile()); + final Path manifestPath = assetRoot.resolve("asset.json"); + + final ObjectMapper mapper = new ObjectMapper(); + final ObjectNode manifest = (ObjectNode) mapper.readTree(manifestPath.toFile()); + final ObjectNode inputs = manifest.putObject("inputs"); + inputs.putArray("sprites").add("a.png"); + final var artifacts = manifest.putArray("artifacts"); + artifacts.addObject().put("file", "b.png").put("index", 0); + artifacts.addObject().put("file", "a.png").put("index", 1); + mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest); + + final PackerAssetDetailsService service = service(); + final var result = service.getAssetDetails(new GetAssetDetailsRequest(project(projectRoot), AssetReference.forAssetId(1))); + + assertEquals(PackerOperationStatus.SUCCESS, result.status()); + assertEquals( + List.of("b.png", "a.png"), + result.details().bankComposition().selectedFiles().stream().map(file -> file.path()).toList()); + } + @Test void returnsUnregisteredDetailsForValidUnregisteredRootReference() throws Exception { final Path projectRoot = copyFixture("workspaces/orphan-valid", tempDir.resolve("orphan")); diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionControl.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionControl.java index f6380154..cd2ca561 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionControl.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/bank/AssetDetailsBankCompositionControl.java @@ -1,8 +1,10 @@ package p.studio.workspaces.assets.details.bank; +import javafx.application.Platform; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; +import p.packer.messages.ApplyBankCompositionRequest; import p.studio.Container; import p.studio.controls.banks.StudioAssetCapacityMeter; import p.studio.controls.forms.StudioFormActionBar; @@ -18,12 +20,14 @@ import p.studio.workspaces.assets.messages.events.StudioAssetBankCompositionAppl import p.studio.workspaces.assets.messages.events.StudioAssetBankCompositionApplyRequestedEvent; import p.studio.workspaces.assets.messages.events.StudioAssetBankCompositionCapacityChangedEvent; import p.studio.workspaces.assets.messages.events.StudioAssetBankCompositionDraftChangedEvent; +import p.studio.workspaces.assets.messages.events.StudioAssetsRefreshRequestedEvent; import p.studio.workspaces.assets.messages.events.StudioAssetsDetailsViewStateChangedEvent; import p.studio.workspaces.framework.StudioSubscriptionBag; import java.util.Objects; public final class AssetDetailsBankCompositionControl extends VBox implements StudioControlLifecycle { + private final ProjectReference projectReference; private final StudioWorkspaceEventBus workspaceBus; private final StudioSubscriptionBag subscriptions = new StudioSubscriptionBag(); private final StudioFormActionBar actionBar = new StudioFormActionBar( @@ -39,7 +43,7 @@ public final class AssetDetailsBankCompositionControl extends VBox implements St public AssetDetailsBankCompositionControl(ProjectReference projectReference, StudioWorkspaceEventBus workspaceBus) { StudioControlLifecycleSupport.install(this, this); - Objects.requireNonNull(projectReference, "projectReference"); + this.projectReference = Objects.requireNonNull(projectReference, "projectReference"); this.workspaceBus = Objects.requireNonNull(workspaceBus, "workspaceBus"); } @@ -126,21 +130,40 @@ public final class AssetDetailsBankCompositionControl extends VBox implements St } private void applyDraft() { - if (viewState == null || viewState.selectedAssetReference() == null) { + if (viewState == null || viewState.selectedAssetReference() == null || !coordinator.ready()) { return; } - workspaceBus.publish(new StudioAssetBankCompositionApplyRequestedEvent(viewState.selectedAssetReference())); - try { - coordinator.apply(); - render(); - publishStateNotifications(); - workspaceBus.publish(new StudioAssetBankCompositionAppliedEvent(viewState.selectedAssetReference())); - } catch (RuntimeException exception) { - workspaceBus.publish(new StudioAssetBankCompositionApplyFailedEvent( - viewState.selectedAssetReference(), - exception.getMessage())); - throw exception; - } + final var assetReference = viewState.selectedAssetReference(); + final var selectedPaths = coordinator.viewModel().selectedFiles().stream() + .map(file -> file.path()) + .toList(); + workspaceBus.publish(new StudioAssetBankCompositionApplyRequestedEvent(assetReference)); + Container.backgroundTasks().submit(() -> { + final var request = new ApplyBankCompositionRequest( + projectReference.toPackerProjectContext(), + assetReference, + selectedPaths); + try { + final var response = Container.packer().workspaceService().applyBankComposition(request); + Platform.runLater(() -> { + if (response.success()) { + coordinator.apply(); + render(); + publishStateNotifications(); + workspaceBus.publish(new StudioAssetBankCompositionAppliedEvent(assetReference)); + workspaceBus.publish(new StudioAssetsRefreshRequestedEvent(assetReference)); + } else { + workspaceBus.publish(new StudioAssetBankCompositionApplyFailedEvent( + assetReference, + response.errorMessage())); + } + }); + } catch (Exception exception) { + Platform.runLater(() -> workspaceBus.publish(new StudioAssetBankCompositionApplyFailedEvent( + assetReference, + exception.getMessage()))); + } + }); } private void resetDraft() {