implements PR-10f bank composition apply through packer and snapshot refresh
This commit is contained in:
parent
401f27a5bb
commit
2fa604e308
@ -20,4 +20,6 @@ public interface PackerWorkspaceService {
|
||||
DeleteAssetResult deleteAsset(DeleteAssetRequest request);
|
||||
|
||||
UpdateAssetContractResponse updateAssetContract(UpdateAssetContractRequest request);
|
||||
|
||||
ApplyBankCompositionResponse applyBankComposition(ApplyBankCompositionRequest request);
|
||||
}
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
package p.packer.messages;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record ApplyBankCompositionRequest(
|
||||
PackerProjectContext project,
|
||||
AssetReference assetReference,
|
||||
List<String> selectedFiles) {
|
||||
|
||||
public ApplyBankCompositionRequest {
|
||||
Objects.requireNonNull(project, "project");
|
||||
Objects.requireNonNull(assetReference, "assetReference");
|
||||
selectedFiles = List.copyOf(Objects.requireNonNull(selectedFiles, "selectedFiles"));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
package p.packer.messages;
|
||||
|
||||
public record ApplyBankCompositionResponse(
|
||||
boolean success,
|
||||
String errorMessage) {
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -15,6 +15,7 @@ public record PackerAssetDeclaration(
|
||||
String name,
|
||||
AssetFamilyCatalog assetFamily,
|
||||
Map<String, List<String>> inputsByRole,
|
||||
List<PackerAssetArtifactSelection> artifacts,
|
||||
OutputFormatCatalog outputFormat,
|
||||
OutputCodecCatalog outputCodec,
|
||||
Map<String, String> 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"));
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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<PackerAssetArtifactSelection> optionalArtifacts(
|
||||
final JsonNode node,
|
||||
final List<PackerDiagnostic> 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<PackerAssetArtifactSelection> 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("..");
|
||||
|
||||
@ -150,7 +150,7 @@ public final class PackerAssetDetailsService {
|
||||
.map(this::toBankCompositionFile)
|
||||
.toList();
|
||||
|
||||
final List<PackerBankCompositionFile> selectedFiles = flattenSelectedInputPaths(declaration.inputsByRole()).stream()
|
||||
final List<PackerBankCompositionFile> 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<String> flattenSelectedInputPaths(Map<String, List<String>> inputsByRole) {
|
||||
private List<String> selectedPathsFor(PackerAssetDeclaration declaration) {
|
||||
if (!declaration.artifacts().isEmpty()) {
|
||||
return declaration.artifacts().stream()
|
||||
.sorted(Comparator.comparingInt(PackerAssetArtifactSelection::index))
|
||||
.map(PackerAssetArtifactSelection::file)
|
||||
.toList();
|
||||
}
|
||||
final List<String> selected = new ArrayList<>();
|
||||
inputsByRole.values().forEach(selected::addAll);
|
||||
declaration.inputsByRole().values().forEach(selected::addAll);
|
||||
return List.copyOf(selected);
|
||||
}
|
||||
|
||||
|
||||
@ -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"));
|
||||
|
||||
@ -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"));
|
||||
|
||||
@ -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() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user