From bf4dc1746913336dfbfe8baf6a53df6fde42b654 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 16 Mar 2026 09:15:40 +0000 Subject: [PATCH] added move action into asset workspace --- ...t-action-wizard-and-fs-first-relocation.md | 101 ++ docs/packer/pull-requests/README.md | 3 +- .../java/p/packer/PackerWorkspaceService.java | 2 + .../p/packer/messages/AssetReference.java | 11 + .../p/packer/messages/MoveAssetRequest.java | 18 + .../p/packer/messages/MoveAssetResult.java | 26 + .../p/packer/messages/assets/AssetAction.java | 1 + .../models/PackerMoveAssetEvaluation.java | 28 + .../FileSystemPackerWorkspaceService.java | 99 ++ .../PackerAssetActionReadService.java | 138 ++- .../services/PackerRuntimePatchService.java | 30 + .../FileSystemPackerWorkspaceServiceTest.java | 150 ++- .../java/p/studio/utilities/i18n/I18n.java | 7 + .../workspaces/assets/AssetWorkspace.java | 2 +- .../assets/details/AssetDetailsControl.java | 29 +- .../assets/details/AssetDetailsUiSupport.java | 1 + .../assets/list/AssetListItemControl.java | 2 +- ...ssetsWorkspaceSelectionRequestedEvent.java | 3 +- .../assets/wizards/MoveAssetWizard.java | 420 ++++++++ .../main/resources/i18n/messages.properties | 9 +- test-projects/main/.studio/activities.json | 935 +++++++++++------- .../main/assets/.prometeu/index.json | 9 +- .../main/assets/recovered/bbb2/asset.json | 14 + .../main/assets/{ => ui}/sound/asset.json | 0 24 files changed, 1651 insertions(+), 387 deletions(-) create mode 100644 docs/packer/pull-requests/PR-23-move-asset-action-wizard-and-fs-first-relocation.md create mode 100644 prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/MoveAssetRequest.java create mode 100644 prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/MoveAssetResult.java create mode 100644 prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerMoveAssetEvaluation.java create mode 100644 prometeu-studio/src/main/java/p/studio/workspaces/assets/wizards/MoveAssetWizard.java create mode 100644 test-projects/main/assets/recovered/bbb2/asset.json rename test-projects/main/assets/{ => ui}/sound/asset.json (100%) diff --git a/docs/packer/pull-requests/PR-23-move-asset-action-wizard-and-fs-first-relocation.md b/docs/packer/pull-requests/PR-23-move-asset-action-wizard-and-fs-first-relocation.md new file mode 100644 index 00000000..de8712b2 --- /dev/null +++ b/docs/packer/pull-requests/PR-23-move-asset-action-wizard-and-fs-first-relocation.md @@ -0,0 +1,101 @@ +# PR-23 Move Asset Action Wizard and Fs-First Relocation + +Domain Owner: `docs/packer` +Cross-Domain Impact: `docs/studio` + +## Briefing + +The action capability track already delivers `REGISTER` and `DELETE`. + +The next operational action is `MOVE`, which must let the user relocate an asset root inside the project's `assets/` tree and optionally rename the asset directory in the same flow. + +Studio should own the wizard and the interaction flow. The packer should own the validation semantics, the filesystem move, the registry update, and the point snapshot patch after durable commit. + +## Objective + +Deliver `AssetAction.MOVE` end to end with a Studio relocation wizard, packer-owned constraints, filesystem-first directory move, and minimal in-memory snapshot update. + +## Dependencies + +- [`./PR-20-asset-action-capabilities-and-register-first-delivery.md`](./PR-20-asset-action-capabilities-and-register-first-delivery.md) +- [`./PR-21-point-in-memory-snapshot-updates-after-write-commit.md`](./PR-21-point-in-memory-snapshot-updates-after-write-commit.md) +- [`./PR-22-delete-asset-action-confirmation-and-fs-first-manifest-removal.md`](./PR-22-delete-asset-action-confirmation-and-fs-first-manifest-removal.md) + +## Scope + +- extend the public action contract and write messages for `MOVE` +- expose `MOVE` capability from the packer +- add a Studio wizard that collects: + - destination parent directory + - destination directory name + - derived target root inside `assets/` +- add a confirmation step before execution +- add an execution/waiting step with spinner while the move is running +- allow the wizard to work as both relocate and rename of the asset directory +- enforce packer constraints that: + - the target must stay inside the project's `assets/` + - the target root must not already contain `asset.json` +- perform the actual move in the filesystem inside the packer write lane +- update `index.json` and patch the runtime snapshot minimally after commit + +## Non-Goals + +- no move outside the project's `assets/` +- no directory merge behavior +- no recursive validation of non-manifest file contents +- no batch move +- no frontend-local move semantics + +## Execution Method + +1. Extend the action contract with `MOVE` and add write request/response messages for asset relocation. +2. Add packer capability resolution for `MOVE`. +3. Build a Studio wizard dedicated to move/rename, with: + - current asset root display + - destination parent picker constrained to `assets/` + - destination directory name field + - derived target root preview + - validation feedback + - confirmation step before submit + - waiting state with spinner after submit until the write result is known +4. Enforce these constraints in the packer: + - the target root must remain under `assets/` + - the target root must not already contain `asset.json` +5. Execute the move in the packer write lane by: + - moving the asset directory in the filesystem + - updating the registry entry root when the asset is registered + - preserving `asset_uuid` +6. Patch the runtime snapshot in memory after the move, without whole-project reload in the normal path. +7. Let Studio refresh and reselect the moved asset after success. +8. Keep the wizard open in waiting state while the move is running, then close only after success; on failure, leave the wizard recoverable with feedback. + +## Acceptance Criteria + +- `MOVE` is exposed through the packer action capability contract +- Studio opens a wizard for `MOVE` +- the wizard has an explicit confirmation step before execution +- the wizard enters a waiting state with spinner while the move is in flight +- the wizard lets the user relocate and/or rename the asset directory +- the move target cannot be outside the project's `assets/` +- the move target cannot already contain `asset.json` +- the packer performs the filesystem move +- `index.json` is updated when needed +- the runtime snapshot is patched minimally after success +- Studio refreshes and keeps the moved asset selected + +## Validation + +- packer tests for move capability visibility +- packer tests for successful relocate and successful rename +- packer tests for blockers when target is outside `assets/` +- packer tests for blockers when target already contains `asset.json` +- packer tests proving snapshot patching after move +- Studio smoke validation for wizard confirmation flow, waiting state, and post-move reselection + +## 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/main/resources/**` diff --git a/docs/packer/pull-requests/README.md b/docs/packer/pull-requests/README.md index db752620..af0cc4ae 100644 --- a/docs/packer/pull-requests/README.md +++ b/docs/packer/pull-requests/README.md @@ -82,6 +82,7 @@ The current production track for the standalone `prometeu-packer` project is: 20. [`PR-20-asset-action-capabilities-and-register-first-delivery.md`](./PR-20-asset-action-capabilities-and-register-first-delivery.md) 21. [`PR-21-point-in-memory-snapshot-updates-after-write-commit.md`](./PR-21-point-in-memory-snapshot-updates-after-write-commit.md) 22. [`PR-22-delete-asset-action-confirmation-and-fs-first-manifest-removal.md`](./PR-22-delete-asset-action-confirmation-and-fs-first-manifest-removal.md) +23. [`PR-23-move-asset-action-wizard-and-fs-first-relocation.md`](./PR-23-move-asset-action-wizard-and-fs-first-relocation.md) Current wave discipline from `PR-11` onward: @@ -92,4 +93,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-20 -> PR-21 -> PR-22` +`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 -> PR-21 -> PR-22 -> PR-23` 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 fa47ec31..fcf805b1 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 @@ -15,5 +15,7 @@ public interface PackerWorkspaceService { RegisterAssetResult registerAsset(RegisterAssetRequest request); + MoveAssetResult moveAsset(MoveAssetRequest request); + DeleteAssetResult deleteAsset(DeleteAssetRequest request); } diff --git a/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/AssetReference.java b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/AssetReference.java index 29633039..858c9549 100644 --- a/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/AssetReference.java +++ b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/AssetReference.java @@ -49,4 +49,15 @@ public record AssetReference(String value) { } return value; } + + @Override + public boolean equals(Object o) { + if (!(o instanceof AssetReference(String v1))) return false; + return value.equals(v1); + } + + @Override + public int hashCode() { + return value.hashCode(); + } } diff --git a/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/MoveAssetRequest.java b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/MoveAssetRequest.java new file mode 100644 index 00000000..366c4471 --- /dev/null +++ b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/MoveAssetRequest.java @@ -0,0 +1,18 @@ +package p.packer.messages; + +import java.util.Objects; + +public record MoveAssetRequest( + PackerProjectContext project, + AssetReference assetReference, + String targetRoot) { + + public MoveAssetRequest { + Objects.requireNonNull(project, "project"); + Objects.requireNonNull(assetReference, "assetReference"); + targetRoot = Objects.requireNonNull(targetRoot, "targetRoot").trim(); + if (targetRoot.isBlank()) { + throw new IllegalArgumentException("targetRoot must not be blank"); + } + } +} diff --git a/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/MoveAssetResult.java b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/MoveAssetResult.java new file mode 100644 index 00000000..86587648 --- /dev/null +++ b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/MoveAssetResult.java @@ -0,0 +1,26 @@ +package p.packer.messages; + +import java.nio.file.Path; +import java.util.Objects; + +public record MoveAssetResult( + PackerOperationStatus status, + String summary, + AssetReference assetReference, + Path assetRoot, + Path manifestPath) { + + public MoveAssetResult { + 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 index 1e04d8cc..d615ee71 100644 --- 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 @@ -2,5 +2,6 @@ package p.packer.messages.assets; public enum AssetAction { REGISTER, + MOVE, DELETE } diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerMoveAssetEvaluation.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerMoveAssetEvaluation.java new file mode 100644 index 00000000..07e83bdf --- /dev/null +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerMoveAssetEvaluation.java @@ -0,0 +1,28 @@ +package p.packer.models; + +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public record PackerMoveAssetEvaluation( + PackerResolvedAssetReference resolved, + List diagnostics, + boolean canMove, + String reason, + Optional targetRelativeRoot, + Optional targetRoot, + Optional targetManifestPath) { + + public PackerMoveAssetEvaluation { + Objects.requireNonNull(resolved, "resolved"); + diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics")); + reason = reason == null ? null : reason.trim(); + if (reason != null && reason.isBlank()) { + reason = null; + } + targetRelativeRoot = Objects.requireNonNull(targetRelativeRoot, "targetRelativeRoot"); + targetRoot = Objects.requireNonNull(targetRoot, "targetRoot"); + targetManifestPath = Objects.requireNonNull(targetManifestPath, "targetManifestPath"); + } +} 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 83ade72a..a5d39ed7 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 @@ -7,6 +7,8 @@ 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.MoveAssetRequest; +import p.packer.messages.MoveAssetResult; import p.packer.messages.assets.OutputFormatCatalog; import p.packer.messages.assets.PackerAssetState; import p.packer.messages.assets.PackerBuildParticipation; @@ -31,6 +33,7 @@ import p.packer.PackerWorkspaceService; import p.packer.models.PackerAssetDeclarationParseResult; import p.packer.models.PackerDeleteAssetEvaluation; import p.packer.models.PackerAssetIdentity; +import p.packer.models.PackerMoveAssetEvaluation; import p.packer.models.PackerRegisterAssetEvaluation; import p.packer.models.PackerAssetSummary; import p.packer.models.PackerDiagnostic; @@ -171,6 +174,14 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe return writeCoordinator.execute(project, () -> registerAssetInWriteLane(safeRequest, events)); } + @Override + public MoveAssetResult moveAsset(MoveAssetRequest request) { + final MoveAssetRequest safeRequest = Objects.requireNonNull(request, "request"); + final PackerProjectContext project = safeRequest.project(); + final PackerOperationEventEmitter events = new PackerOperationEventEmitter(project, eventSink); + return writeCoordinator.execute(project, () -> moveAssetInWriteLane(safeRequest, events)); + } + @Override public DeleteAssetResult deleteAsset(DeleteAssetRequest request) { final DeleteAssetRequest safeRequest = Objects.requireNonNull(request, "request"); @@ -352,6 +363,69 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe } } + private MoveAssetResult moveAssetInWriteLane( + MoveAssetRequest request, + PackerOperationEventEmitter events) { + final PackerProjectContext project = request.project(); + workspaceFoundation.initWorkspace(new InitWorkspaceRequest(project)); + final PackerRuntimeSnapshot snapshot = runtimeRegistry.getOrLoad(project).snapshot(); + final PackerMoveAssetEvaluation evaluation = actionReadService.evaluateMove(snapshot, project, request.assetReference(), request.targetRoot()); + if (!evaluation.canMove()) { + return moveFailureResult( + events, + Objects.requireNonNullElse(evaluation.reason(), "Asset cannot be moved."), + evaluation.resolved().assetRoot(), + evaluation.resolved().assetRoot().resolve("asset.json"), + List.of(relativeAssetRoot(project, evaluation.resolved().assetRoot()))); + } + + final Path sourceRoot = evaluation.resolved().assetRoot(); + final Path targetRoot = evaluation.targetRoot().orElseThrow(); + final Path targetManifestPath = evaluation.targetManifestPath().orElseThrow(); + final String sourceRelativeRoot = relativeAssetRoot(project, sourceRoot); + final String targetRelativeRoot = evaluation.targetRelativeRoot().orElseThrow(); + try { + Files.createDirectories(targetRoot.getParent()); + Files.move(sourceRoot, targetRoot); + + final PackerRegistryState registry = workspaceFoundation.loadRegistry(project); + final Optional updatedEntry = evaluation.resolved().registryEntry() + .map(entry -> new PackerRegistryEntry(entry.assetId(), entry.assetUuid(), targetRelativeRoot, entry.includedInBuild())); + final PackerRegistryState updatedRegistry = updatedEntry + .map(entry -> replaceRegistryEntryRoot(registry, entry)) + .orElse(registry); + if (!updatedRegistry.equals(registry)) { + workspaceFoundation.saveRegistry(project, updatedRegistry); + } + runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterMoveAsset( + currentSnapshot, + generation, + updatedRegistry, + updatedEntry, + sourceRoot, + targetRoot, + targetManifestPath)); + final AssetReference canonicalReference = updatedEntry.isPresent() + ? AssetReference.forAssetId(updatedEntry.get().assetId()) + : AssetReference.forRelativeAssetRoot(targetRelativeRoot); + final MoveAssetResult result = new MoveAssetResult( + PackerOperationStatus.SUCCESS, + "Asset moved: " + sourceRelativeRoot + " -> " + targetRelativeRoot, + canonicalReference, + targetRoot, + targetManifestPath); + events.emit(PackerEventKind.ACTION_APPLIED, result.summary(), List.of(sourceRelativeRoot, targetRelativeRoot)); + return result; + } catch (IOException exception) { + return moveFailureResult( + events, + "Unable to move asset: " + exception.getMessage(), + sourceRoot, + sourceRoot.resolve("asset.json"), + List.of(sourceRelativeRoot, targetRelativeRoot)); + } + } + private CreateAssetResult failureResult( PackerOperationEventEmitter events, String summary, @@ -399,6 +473,22 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe return result; } + private MoveAssetResult moveFailureResult( + PackerOperationEventEmitter events, + String summary, + Path assetRoot, + Path manifestPath, + List affectedAssets) { + final MoveAssetResult result = new MoveAssetResult( + 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); @@ -504,4 +594,13 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe .toList(); return registry.withAssets(updatedEntries, registry.nextAssetId()); } + + private PackerRegistryState replaceRegistryEntryRoot( + PackerRegistryState registry, + PackerRegistryEntry updatedEntry) { + final List updatedEntries = registry.assets().stream() + .map(candidate -> candidate.assetId() == updatedEntry.assetId() ? updatedEntry : candidate) + .toList(); + return registry.withAssets(updatedEntries, registry.nextAssetId()); + } } 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 index 44c89b48..93af935f 100644 --- 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 @@ -1,6 +1,5 @@ package p.packer.services; -import p.packer.dtos.PackerAssetActionAvailabilityDTO; import p.packer.messages.AssetReference; import p.packer.messages.GetAssetActionsRequest; import p.packer.messages.GetAssetActionsResult; @@ -14,6 +13,7 @@ import p.packer.models.PackerAssetDeclaration; import p.packer.models.PackerAssetDeclarationParseResult; import p.packer.models.PackerDeleteAssetEvaluation; import p.packer.models.PackerDiagnostic; +import p.packer.models.PackerMoveAssetEvaluation; import p.packer.models.PackerRegisterAssetEvaluation; import p.packer.models.PackerRegistryEntry; import p.packer.models.PackerResolvedAssetReference; @@ -44,6 +44,7 @@ public final class PackerAssetActionReadService { final PackerProjectContext project = Objects.requireNonNull(request, "request").project(); final PackerRuntimeSnapshot snapshot = runtimeRegistry.getOrLoad(project).snapshot(); final PackerRegisterAssetEvaluation registerEvaluation = evaluateRegister(snapshot, project, request.assetReference()); + final PackerMoveAssetEvaluation moveEvaluation = evaluateMove(snapshot, project, request.assetReference(), null); final PackerDeleteAssetEvaluation deleteEvaluation = evaluateDelete(snapshot, project, request.assetReference()); final List actions = new ArrayList<>(); if (registerEvaluation.resolved().registryEntry().isEmpty()) { @@ -53,6 +54,13 @@ public final class PackerAssetActionReadService { true, registerEvaluation.reason())); } + if (moveEvaluation.resolved().runtimeAsset().isPresent()) { + actions.add(new PackerAssetActionAvailability( + AssetAction.MOVE, + moveEvaluation.canMove(), + true, + moveEvaluation.reason())); + } if (deleteEvaluation.resolved().runtimeAsset().isPresent()) { actions.add(new PackerAssetActionAvailability( AssetAction.DELETE, @@ -62,9 +70,12 @@ public final class PackerAssetActionReadService { } final PackerOperationStatus status; - if (registerEvaluation.resolved().runtimeAsset().isEmpty() && deleteEvaluation.resolved().runtimeAsset().isEmpty()) { + if (registerEvaluation.resolved().runtimeAsset().isEmpty() + && moveEvaluation.resolved().runtimeAsset().isEmpty() + && deleteEvaluation.resolved().runtimeAsset().isEmpty()) { status = PackerOperationStatus.FAILED; - } else if (combinedDiagnostics(registerEvaluation.diagnostics(), deleteEvaluation.diagnostics()).stream().anyMatch(PackerDiagnostic::blocking)) { + } else if (combinedDiagnostics(registerEvaluation.diagnostics(), moveEvaluation.diagnostics(), deleteEvaluation.diagnostics()) + .stream().anyMatch(PackerDiagnostic::blocking)) { status = PackerOperationStatus.PARTIAL; } else { status = PackerOperationStatus.SUCCESS; @@ -73,7 +84,10 @@ public final class PackerAssetActionReadService { status, "Asset action capabilities resolved from runtime snapshot.", PackerReadMessageMapper.toAssetActionAvailabilityDTOs(actions), - PackerReadMessageMapper.toDiagnosticDTOs(combinedDiagnostics(registerEvaluation.diagnostics(), deleteEvaluation.diagnostics()))); + PackerReadMessageMapper.toDiagnosticDTOs(combinedDiagnostics( + registerEvaluation.diagnostics(), + moveEvaluation.diagnostics(), + deleteEvaluation.diagnostics()))); } public PackerRegisterAssetEvaluation evaluateRegister( @@ -153,6 +167,98 @@ public final class PackerAssetActionReadService { return new PackerDeleteAssetEvaluation(resolved, diagnostics, true, null); } + public PackerMoveAssetEvaluation evaluateMove( + PackerRuntimeSnapshot snapshot, + PackerProjectContext project, + AssetReference assetReference, + String requestedTargetRoot) { + final PackerResolvedAssetReference resolved = assetReferenceResolver.resolve(project, snapshot, assetReference); + final List diagnostics = new ArrayList<>(resolved.diagnostics()); + 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 PackerMoveAssetEvaluation( + resolved, + diagnostics, + false, + "asset.json was not found for the requested asset root.", + Optional.empty(), + Optional.empty(), + Optional.empty()); + } + + if (requestedTargetRoot == null) { + return new PackerMoveAssetEvaluation( + resolved, + diagnostics, + true, + null, + Optional.empty(), + Optional.empty(), + Optional.empty()); + } + + final String normalizedTargetRoot = normalizeRelativeAssetRoot(requestedTargetRoot); + if (normalizedTargetRoot == null) { + return new PackerMoveAssetEvaluation( + resolved, + diagnostics, + false, + "Target root must stay inside assets/ and use a non-blank relative path.", + Optional.empty(), + Optional.empty(), + Optional.empty()); + } + + final Path targetRoot = PackerWorkspacePaths.assetRoot(project, normalizedTargetRoot); + if (!targetRoot.startsWith(PackerWorkspacePaths.assetsRoot(project))) { + return new PackerMoveAssetEvaluation( + resolved, + diagnostics, + false, + "Target root must stay inside assets/.", + Optional.of(normalizedTargetRoot), + Optional.of(targetRoot), + Optional.of(targetRoot.resolve("asset.json"))); + } + + if (targetRoot.equals(resolved.assetRoot())) { + return new PackerMoveAssetEvaluation( + resolved, + diagnostics, + false, + "Target root must differ from the current asset root.", + Optional.of(normalizedTargetRoot), + Optional.of(targetRoot), + Optional.of(targetRoot.resolve("asset.json"))); + } + + final Path targetManifestPath = targetRoot.resolve("asset.json"); + if (java.nio.file.Files.isRegularFile(targetManifestPath)) { + return new PackerMoveAssetEvaluation( + resolved, + diagnostics, + false, + "Target root already contains asset.json.", + Optional.of(normalizedTargetRoot), + Optional.of(targetRoot), + Optional.of(targetManifestPath)); + } + + return new PackerMoveAssetEvaluation( + resolved, + diagnostics, + true, + null, + Optional.of(normalizedTargetRoot), + Optional.of(targetRoot), + Optional.of(targetManifestPath)); + } + private String firstBlockingReason(List diagnostics, String fallback) { return diagnostics.stream() .filter(PackerDiagnostic::blocking) @@ -162,11 +268,25 @@ public final class PackerAssetActionReadService { .orElse(fallback); } - private List combinedDiagnostics( - List left, - List right) { - final List combined = new ArrayList<>(left); - combined.addAll(right); + @SafeVarargs + private List combinedDiagnostics(List... groups) { + final List combined = new ArrayList<>(); + for (List group : groups) { + combined.addAll(group); + } return combined; } + + private String normalizeRelativeAssetRoot(String candidate) { + final String raw = Objects.requireNonNullElse(candidate, "").trim().replace('\\', '/'); + if (raw.isBlank()) { + return null; + } + final Path normalized = Path.of(raw).normalize(); + if (normalized.isAbsolute() || normalized.startsWith("..")) { + return null; + } + final String value = normalized.toString().replace('\\', '/'); + return value.isBlank() ? null : value; + } } diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerRuntimePatchService.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerRuntimePatchService.java index 77748eb2..4ec02dbd 100644 --- a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerRuntimePatchService.java +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerRuntimePatchService.java @@ -76,4 +76,34 @@ public final class PackerRuntimePatchService { .toList(); return new PackerRuntimeSnapshot(generation, updatedRegistry, updatedAssets); } + + public PackerRuntimeSnapshot afterMoveAsset( + PackerRuntimeSnapshot snapshot, + long generation, + PackerRegistryState updatedRegistry, + java.util.Optional updatedRegistryEntry, + Path sourceRoot, + Path targetRoot, + Path targetManifestPath) { + final PackerAssetDeclarationParseResult parsed = declarationParser.parse(targetManifestPath); + final List updatedAssets = new ArrayList<>(); + boolean patched = false; + for (PackerRuntimeAsset asset : snapshot.assets()) { + if (asset.assetRoot().equals(sourceRoot.toAbsolutePath().normalize())) { + updatedAssets.add(new PackerRuntimeAsset( + targetRoot, + targetManifestPath, + updatedRegistryEntry, + parsed)); + patched = true; + } else { + updatedAssets.add(asset); + } + } + if (!patched) { + throw new IllegalStateException("Unable to patch runtime snapshot for moved asset: " + sourceRoot); + } + updatedAssets.sort(Comparator.comparing(asset -> asset.assetRoot().toString(), String.CASE_INSENSITIVE_ORDER)); + return new PackerRuntimeSnapshot(generation, updatedRegistry, updatedAssets); + } } 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 20ce5d21..01bbfe5c 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 @@ -19,6 +19,8 @@ import p.packer.messages.DeleteAssetResult; import p.packer.messages.GetAssetActionsRequest; import p.packer.messages.GetAssetDetailsRequest; import p.packer.messages.ListAssetsRequest; +import p.packer.messages.MoveAssetRequest; +import p.packer.messages.MoveAssetResult; import p.packer.messages.RegisterAssetRequest; import p.packer.messages.RegisterAssetResult; import p.packer.messages.assets.AssetAction; @@ -201,15 +203,16 @@ final class FileSystemPackerWorkspaceServiceTest { AssetReference.forRelativeAssetRoot("orphans/ui_sounds"))); assertEquals(PackerOperationStatus.SUCCESS, result.status()); - assertEquals(2, result.actions().size()); + assertEquals(3, result.actions().size()); assertEquals(AssetAction.REGISTER, result.actions().get(0).action()); assertTrue(result.actions().get(0).visible()); assertTrue(result.actions().get(0).enabled()); assertNull(result.actions().get(0).reason()); - assertEquals(AssetAction.DELETE, result.actions().get(1).action()); + assertEquals(AssetAction.MOVE, result.actions().get(1).action()); assertTrue(result.actions().get(1).visible()); assertTrue(result.actions().get(1).enabled()); assertNull(result.actions().get(1).reason()); + assertEquals(AssetAction.DELETE, result.actions().get(2).action()); } @Test @@ -222,9 +225,11 @@ final class FileSystemPackerWorkspaceServiceTest { AssetReference.forAssetId(1))); assertEquals(PackerOperationStatus.SUCCESS, result.status()); - assertEquals(1, result.actions().size()); - assertEquals(AssetAction.DELETE, result.actions().getFirst().action()); - assertTrue(result.actions().getFirst().enabled()); + assertEquals(2, result.actions().size()); + assertEquals(AssetAction.MOVE, result.actions().get(0).action()); + assertTrue(result.actions().get(0).enabled()); + assertEquals(AssetAction.DELETE, result.actions().get(1).action()); + assertTrue(result.actions().get(1).enabled()); } @Test @@ -270,8 +275,9 @@ final class FileSystemPackerWorkspaceServiceTest { final var actionsResult = service.getAssetActions(new GetAssetActionsRequest( project(projectRoot), registerResult.assetReference())); - assertEquals(1, actionsResult.actions().size()); - assertEquals(AssetAction.DELETE, actionsResult.actions().getFirst().action()); + assertEquals(2, actionsResult.actions().size()); + assertEquals(AssetAction.MOVE, actionsResult.actions().get(0).action()); + assertEquals(AssetAction.DELETE, actionsResult.actions().get(1).action()); assertEquals(1, loader.loadCount()); } @@ -285,12 +291,13 @@ final class FileSystemPackerWorkspaceServiceTest { AssetReference.forRelativeAssetRoot("bad"))); assertEquals(PackerOperationStatus.PARTIAL, result.status()); - assertEquals(2, result.actions().size()); + assertEquals(3, result.actions().size()); assertEquals(AssetAction.REGISTER, result.actions().get(0).action()); assertFalse(result.actions().get(0).enabled()); assertNotNull(result.actions().get(0).reason()); - assertEquals(AssetAction.DELETE, result.actions().get(1).action()); + assertEquals(AssetAction.MOVE, result.actions().get(1).action()); assertTrue(result.actions().get(1).enabled()); + assertEquals(AssetAction.DELETE, result.actions().get(2).action()); } @Test @@ -350,12 +357,13 @@ final class FileSystemPackerWorkspaceServiceTest { final var actions = service.getAssetActions(new GetAssetActionsRequest( project(projectRoot), AssetReference.forRelativeAssetRoot("orphans/ui_clone"))); - assertEquals(2, actions.actions().size()); + assertEquals(3, actions.actions().size()); assertEquals(AssetAction.REGISTER, actions.actions().get(0).action()); assertFalse(actions.actions().get(0).enabled()); assertNotNull(actions.actions().get(0).reason()); - assertEquals(AssetAction.DELETE, actions.actions().get(1).action()); + assertEquals(AssetAction.MOVE, actions.actions().get(1).action()); assertTrue(actions.actions().get(1).enabled()); + assertEquals(AssetAction.DELETE, actions.actions().get(2).action()); final RegisterAssetResult registerResult = service.registerAsset(new RegisterAssetRequest( project(projectRoot), @@ -393,6 +401,126 @@ final class FileSystemPackerWorkspaceServiceTest { assertTrue(listResult.assets().isEmpty()); } + @Test + void movesRegisteredAssetAndKeepsRegisteredIdentity() throws Exception { + final Path projectRoot = tempDir.resolve("move-registered"); + final FileSystemPackerWorkspaceService service = service(); + final CreateAssetResult createResult = service.createAsset(new CreateAssetRequest( + project(projectRoot), + "ui/source-atlas", + "source_atlas", + AssetFamilyCatalog.IMAGE_BANK, + OutputFormatCatalog.TILES_INDEXED_V1, + OutputCodecCatalog.NONE, + true)); + Files.writeString(projectRoot.resolve("assets/ui/source-atlas/atlas.png"), "fixture"); + + final MoveAssetResult moveResult = service.moveAsset(new MoveAssetRequest( + project(projectRoot), + createResult.assetReference(), + "ui/renamed-atlas")); + + assertEquals(PackerOperationStatus.SUCCESS, moveResult.status()); + assertEquals(createResult.assetReference(), moveResult.assetReference()); + assertFalse(Files.exists(projectRoot.resolve("assets/ui/source-atlas/asset.json"))); + assertTrue(Files.isRegularFile(projectRoot.resolve("assets/ui/renamed-atlas/asset.json"))); + assertTrue(Files.isRegularFile(projectRoot.resolve("assets/ui/renamed-atlas/atlas.png"))); + + final var details = service.getAssetDetails(new GetAssetDetailsRequest(project(projectRoot), createResult.assetReference())); + assertEquals(PackerOperationStatus.SUCCESS, details.status()); + assertEquals(PackerAssetState.REGISTERED, details.details().summary().state()); + assertEquals(projectRoot.resolve("assets/ui/renamed-atlas").toAbsolutePath().normalize(), details.details().summary().identity().assetRoot()); + } + + @Test + void movesUnregisteredAssetAndReturnsNewRootReference() throws Exception { + final Path projectRoot = copyFixture("workspaces/orphan-valid", tempDir.resolve("move-unregistered")); + final FileSystemPackerWorkspaceService service = service(); + Files.writeString(projectRoot.resolve("assets/orphans/ui_sounds/readme.txt"), "keep"); + + final MoveAssetResult moveResult = service.moveAsset(new MoveAssetRequest( + project(projectRoot), + AssetReference.forRelativeAssetRoot("orphans/ui_sounds"), + "recovered/ui_sounds_renamed")); + + assertEquals(PackerOperationStatus.SUCCESS, moveResult.status()); + assertEquals(AssetReference.forRelativeAssetRoot("recovered/ui_sounds_renamed"), moveResult.assetReference()); + assertTrue(Files.isRegularFile(projectRoot.resolve("assets/recovered/ui_sounds_renamed/asset.json"))); + assertTrue(Files.isRegularFile(projectRoot.resolve("assets/recovered/ui_sounds_renamed/readme.txt"))); + + final var details = service.getAssetDetails(new GetAssetDetailsRequest(project(projectRoot), moveResult.assetReference())); + assertEquals(PackerOperationStatus.SUCCESS, details.status()); + assertEquals(PackerAssetState.UNREGISTERED, details.details().summary().state()); + assertEquals(projectRoot.resolve("assets/recovered/ui_sounds_renamed").toAbsolutePath().normalize(), details.details().summary().identity().assetRoot()); + } + + @Test + void blocksMoveOutsideAssetsRoot() throws Exception { + final Path projectRoot = copyFixture("workspaces/orphan-valid", tempDir.resolve("move-outside")); + final FileSystemPackerWorkspaceService service = service(); + + final MoveAssetResult moveResult = service.moveAsset(new MoveAssetRequest( + project(projectRoot), + AssetReference.forRelativeAssetRoot("orphans/ui_sounds"), + "../outside/ui_sounds")); + + assertEquals(PackerOperationStatus.FAILED, moveResult.status()); + assertNull(moveResult.assetReference()); + } + + @Test + void blocksMoveWhenTargetAlreadyContainsAssetManifest() throws Exception { + final Path projectRoot = tempDir.resolve("move-target-asset"); + final FileSystemPackerWorkspaceService service = service(); + final CreateAssetResult source = service.createAsset(new CreateAssetRequest( + project(projectRoot), + "ui/source", + "source", + AssetFamilyCatalog.IMAGE_BANK, + OutputFormatCatalog.TILES_INDEXED_V1, + OutputCodecCatalog.NONE, + false)); + final CreateAssetResult target = service.createAsset(new CreateAssetRequest( + project(projectRoot), + "ui/target", + "target", + AssetFamilyCatalog.IMAGE_BANK, + OutputFormatCatalog.TILES_INDEXED_V1, + OutputCodecCatalog.NONE, + false)); + + final MoveAssetResult moveResult = service.moveAsset(new MoveAssetRequest( + project(projectRoot), + source.assetReference(), + "ui/target")); + + assertEquals(PackerOperationStatus.FAILED, moveResult.status()); + assertNull(moveResult.assetReference()); + assertNotNull(target.assetReference()); + } + + @Test + void moveAssetPatchesLoadedSnapshotWithoutWholeProjectReload() throws Exception { + final Path projectRoot = copyFixture("workspaces/orphan-valid", tempDir.resolve("move-no-reload")); + final CountingLoader loader = countingLoader(); + final FileSystemPackerWorkspaceService service = service(ignored -> { }, loader); + + service.listAssets(new ListAssetsRequest(project(projectRoot))); + assertEquals(1, loader.loadCount()); + + final MoveAssetResult moveResult = service.moveAsset(new MoveAssetRequest( + project(projectRoot), + AssetReference.forRelativeAssetRoot("orphans/ui_sounds"), + "recovered/ui_sounds_moved")); + + assertEquals(PackerOperationStatus.SUCCESS, moveResult.status()); + assertEquals(1, loader.loadCount()); + + final var details = service.getAssetDetails(new GetAssetDetailsRequest(project(projectRoot), moveResult.assetReference())); + assertEquals(PackerOperationStatus.SUCCESS, details.status()); + assertEquals(1, loader.loadCount()); + } + @Test void deletesUnregisteredAssetJsonAndRemovesItFromSnapshot() throws Exception { final Path projectRoot = copyFixture("workspaces/orphan-valid", tempDir.resolve("delete-unregistered")); 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 805be9a1..d460702e 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 @@ -213,6 +213,8 @@ public enum I18n { ASSETS_RELOCATE_WIZARD_STEP_DESTINATION_DESCRIPTION("assets.relocateWizard.step.destination.description"), ASSETS_RELOCATE_WIZARD_STEP_SUMMARY_TITLE("assets.relocateWizard.step.summary.title"), ASSETS_RELOCATE_WIZARD_STEP_SUMMARY_DESCRIPTION("assets.relocateWizard.step.summary.description"), + ASSETS_RELOCATE_WIZARD_STEP_WAITING_TITLE("assets.relocateWizard.step.waiting.title"), + ASSETS_RELOCATE_WIZARD_STEP_WAITING_DESCRIPTION("assets.relocateWizard.step.waiting.description"), ASSETS_RELOCATE_WIZARD_LABEL_CURRENT_ROOT("assets.relocateWizard.label.currentRoot"), ASSETS_RELOCATE_WIZARD_LABEL_DESTINATION_PARENT("assets.relocateWizard.label.destinationParent"), ASSETS_RELOCATE_WIZARD_LABEL_DESTINATION_NAME("assets.relocateWizard.label.destinationName"), @@ -223,6 +225,11 @@ public enum I18n { ASSETS_RELOCATE_WIZARD_NOTE("assets.relocateWizard.note"), ASSETS_RELOCATE_WIZARD_BUTTON_CONFIRM("assets.relocateWizard.button.confirm"), ASSETS_RELOCATE_WIZARD_BUTTON_PREVIEW("assets.relocateWizard.button.preview"), + ASSETS_RELOCATE_WIZARD_ERROR_PARENT("assets.relocateWizard.error.parent"), + ASSETS_RELOCATE_WIZARD_ERROR_NAME("assets.relocateWizard.error.name"), + ASSETS_RELOCATE_WIZARD_ERROR_OUTSIDE_ASSETS("assets.relocateWizard.error.outsideAssets"), + ASSETS_RELOCATE_WIZARD_ERROR_TARGET_ALREADY_ASSET("assets.relocateWizard.error.targetAlreadyAsset"), + ASSETS_RELOCATE_WIZARD_ERROR_TARGET_SAME("assets.relocateWizard.error.targetSame"), WORKSPACE_DEBUG("workspace.debug"), diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspace.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspace.java index d0d2a3fd..dfdfcc2f 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspace.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspace.java @@ -119,7 +119,7 @@ public final class AssetWorkspace extends Workspace { } AddAssetWizard.showAndWait(root.getScene().getWindow(), projectReference).ifPresent(assetReference -> { workspaceEventBus.publish(new StudioAssetsRefreshRequestedEvent(assetReference)); - workspaceEventBus.publish(new StudioAssetsWorkspaceSelectionRequestedEvent(assetReference)); + workspaceEventBus.publish(new StudioAssetsWorkspaceSelectionRequestedEvent(assetReference, false)); }); } 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 7aa540d5..c5c7b8d6 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 @@ -8,16 +8,9 @@ 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.DeleteAssetRequest; -import p.packer.messages.DeleteAssetResult; -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.packer.messages.*; import p.studio.Container; import p.studio.events.StudioWorkspaceEventBus; import p.studio.projects.ProjectReference; @@ -32,6 +25,7 @@ import p.studio.workspaces.assets.messages.events.StudioAssetsDetailsViewStateCh import p.studio.workspaces.assets.messages.events.StudioAssetsRefreshRequestedEvent; import p.studio.workspaces.assets.messages.events.StudioAssetsWorkspaceSelectionRequestedEvent; import p.studio.workspaces.assets.wizards.DeleteAssetDialog; +import p.studio.workspaces.assets.wizards.MoveAssetWizard; import p.studio.workspaces.framework.StudioEventAware; import p.studio.workspaces.framework.StudioEventBindings; @@ -110,7 +104,7 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware } }); eventBindings.listen(workspaceBus, StudioAssetsWorkspaceSelectionRequestedEvent.class).handle(event -> { - if (!isCurrentSelection(event.assetReference())) { + if (event.forceUpdate() || !isCurrentSelection(event.assetReference())) { loadSelection(event.assetReference()); } }); @@ -309,10 +303,25 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware renderActions(); Container.backgroundTasks().submit(() -> registerSelectedAsset(viewState.selectedAssetReference())); } + case MOVE -> openMoveWizard(); case DELETE -> confirmDeleteAction(); } } + private void openMoveWizard() { + if (viewState.selectedAssetDetails() == null || getScene() == null || viewState.selectedAssetReference() == null) { + return; + } + MoveAssetWizard.showAndWait( + getScene().getWindow(), + projectReference, + viewState.selectedAssetReference(), + viewState.selectedAssetDetails().summary()).ifPresent(assetReference -> { + workspaceBus.publish(new StudioAssetsRefreshRequestedEvent(assetReference)); + workspaceBus.publish(new StudioAssetsWorkspaceSelectionRequestedEvent(assetReference, true)); + }); + } + private void confirmDeleteAction() { if (viewState.selectedAssetDetails() == null || getScene() == null) { return; @@ -346,7 +355,7 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware 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())); + workspaceBus.publish(new StudioAssetsWorkspaceSelectionRequestedEvent(result.assetReference(), false)); return; } actionFeedbackMessage = Objects.requireNonNullElse(result.summary(), "Unable to register asset."); 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 bab26635..254ec735 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 @@ -108,6 +108,7 @@ public final class AssetDetailsUiSupport { public static String actionLabel(AssetAction action) { return switch (action) { case REGISTER -> Container.i18n().text(I18n.ASSETS_ACTION_REGISTER); + case MOVE -> Container.i18n().text(I18n.ASSETS_ACTION_RELOCATE); case DELETE -> Container.i18n().text(I18n.ASSETS_ACTION_DELETE); }; } diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/list/AssetListItemControl.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/list/AssetListItemControl.java index 572a679f..de72267a 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/assets/list/AssetListItemControl.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/list/AssetListItemControl.java @@ -42,7 +42,7 @@ public final class AssetListItemControl extends VBox { getStyleClass().setAll("assets-workspace-asset-row", assetRowToneClass(summary.assetFamily())); getChildren().setAll(createTopLine(), createPathLabel()); setOnMouseClicked(event -> workspaceBus.publish( - new StudioAssetsWorkspaceSelectionRequestedEvent(summary.assetReference()))); + new StudioAssetsWorkspaceSelectionRequestedEvent(summary.assetReference(), false))); } private HBox createTopLine() { diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/events/StudioAssetsWorkspaceSelectionRequestedEvent.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/events/StudioAssetsWorkspaceSelectionRequestedEvent.java index 9a960fbe..a8e7e911 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/events/StudioAssetsWorkspaceSelectionRequestedEvent.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/events/StudioAssetsWorkspaceSelectionRequestedEvent.java @@ -6,7 +6,8 @@ import p.studio.events.StudioEvent; import java.util.Objects; public record StudioAssetsWorkspaceSelectionRequestedEvent( - AssetReference assetReference) implements StudioEvent { + AssetReference assetReference, + boolean forceUpdate) implements StudioEvent { public StudioAssetsWorkspaceSelectionRequestedEvent { Objects.requireNonNull(assetReference, "assetReference"); } diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/wizards/MoveAssetWizard.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/wizards/MoveAssetWizard.java new file mode 100644 index 00000000..49642b90 --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/wizards/MoveAssetWizard.java @@ -0,0 +1,420 @@ +package p.studio.workspaces.assets.wizards; + +import javafx.application.Platform; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.control.TextField; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.DirectoryChooser; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.Window; +import p.packer.messages.AssetReference; +import p.packer.messages.MoveAssetRequest; +import p.packer.messages.MoveAssetResult; +import p.packer.messages.PackerOperationStatus; +import p.studio.Container; +import p.studio.projects.ProjectReference; +import p.studio.utilities.i18n.I18n; +import p.studio.workspaces.assets.details.AssetDetailsUiSupport; +import p.studio.workspaces.assets.messages.AssetWorkspaceAssetSummary; + +import java.io.File; +import java.nio.file.Path; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + +public final class MoveAssetWizard { + private final ProjectReference projectReference; + private final AssetReference assetReference; + private final AssetWorkspaceAssetSummary assetSummary; + private final Stage stage; + private final AtomicReference result = new AtomicReference<>(); + private final Label stepTitle = new Label(); + private final Label stepDescription = new Label(); + private final VBox stepBody = new VBox(12); + private final Label feedbackLabel = new Label(); + private final Button backButton = new Button(); + private final Button nextButton = new Button(); + private final Button confirmButton = new Button(); + private final TextField destinationParentField = new TextField(); + private final TextField destinationNameField = new TextField(); + private final TextField targetRootField = new TextField(); + + private int stepIndex; + private boolean moving; + + private MoveAssetWizard( + Window owner, + ProjectReference projectReference, + AssetReference assetReference, + AssetWorkspaceAssetSummary assetSummary) { + this.projectReference = Objects.requireNonNull(projectReference, "projectReference"); + this.assetReference = Objects.requireNonNull(assetReference, "assetReference"); + this.assetSummary = Objects.requireNonNull(assetSummary, "assetSummary"); + this.stage = new Stage(); + stage.initOwner(owner); + stage.initModality(Modality.WINDOW_MODAL); + stage.setTitle(Container.i18n().text(I18n.ASSETS_RELOCATE_WIZARD_TITLE)); + stage.setScene(new Scene(buildRoot(), 760, 460)); + stage.getScene().getStylesheets().add(Container.theme().getDefaultTheme()); + stage.setOnCloseRequest(event -> { + if (moving) { + event.consume(); + } + }); + + destinationParentField.setText(initialDestinationParent()); + destinationNameField.setText(assetSummary.assetRoot().getFileName().toString()); + targetRootField.setEditable(false); + renderStep(); + } + + public static Optional showAndWait( + Window owner, + ProjectReference projectReference, + AssetReference assetReference, + AssetWorkspaceAssetSummary assetSummary) { + final MoveAssetWizard wizard = new MoveAssetWizard(owner, projectReference, assetReference, assetSummary); + wizard.stage.showAndWait(); + return Optional.ofNullable(wizard.result.get()); + } + + private VBox buildRoot() { + stepTitle.getStyleClass().add("studio-launcher-section-title"); + stepDescription.getStyleClass().add("studio-launcher-subtitle"); + feedbackLabel.getStyleClass().add("studio-launcher-feedback"); + feedbackLabel.setWrapText(true); + + backButton.textProperty().bind(Container.i18n().bind(I18n.WIZARD_BACK)); + backButton.getStyleClass().addAll("studio-button", "studio-button-secondary"); + backButton.setOnAction(ignored -> goBack()); + + nextButton.textProperty().bind(Container.i18n().bind(I18n.WIZARD_NEXT)); + nextButton.getStyleClass().addAll("studio-button", "studio-button-primary"); + nextButton.setOnAction(ignored -> goNext()); + + confirmButton.textProperty().bind(Container.i18n().bind(I18n.ASSETS_RELOCATE_WIZARD_BUTTON_CONFIRM)); + confirmButton.getStyleClass().addAll("studio-button", "studio-button-primary"); + confirmButton.setOnAction(ignored -> confirmMove()); + + final Button cancelButton = new Button(); + cancelButton.textProperty().bind(Container.i18n().bind(I18n.WIZARD_CANCEL)); + cancelButton.getStyleClass().addAll("studio-button", "studio-button-cancel"); + cancelButton.setOnAction(ignored -> { + if (!moving) { + stage.close(); + } + }); + + final HBox actions = new HBox(12, backButton, nextButton, confirmButton, cancelButton); + actions.setAlignment(Pos.CENTER_RIGHT); + + final VBox root = new VBox(16, stepTitle, stepDescription, stepBody, feedbackLabel, actions); + root.setPadding(new Insets(24)); + VBox.setVgrow(stepBody, Priority.ALWAYS); + return root; + } + + private void renderStep() { + backButton.setDisable(stepIndex == 0 || moving); + nextButton.setVisible(stepIndex == 0 && !moving); + nextButton.setManaged(stepIndex == 0 && !moving); + confirmButton.setVisible(stepIndex == 1 && !moving); + confirmButton.setManaged(stepIndex == 1 && !moving); + if (!moving && stepIndex != 2 && feedbackLabel.getText() == null) { + feedbackLabel.setText(""); + } + + switch (stepIndex) { + case 0 -> renderDestinationStep(); + case 1 -> renderSummaryStep(); + case 2 -> renderWaitingStep(); + default -> throw new IllegalStateException("Unknown wizard step: " + stepIndex); + } + } + + private void renderDestinationStep() { + stepTitle.textProperty().unbind(); + stepDescription.textProperty().unbind(); + stepTitle.textProperty().bind(Container.i18n().bind(I18n.ASSETS_RELOCATE_WIZARD_STEP_DESTINATION_TITLE)); + stepDescription.textProperty().bind(Container.i18n().bind(I18n.ASSETS_RELOCATE_WIZARD_STEP_DESTINATION_DESCRIPTION)); + + targetRootField.setText(displayTargetRoot()); + + final Button browseButton = new Button(); + browseButton.textProperty().bind(Container.i18n().bind(I18n.WIZARD_BROWSE)); + browseButton.getStyleClass().addAll("studio-button", "studio-button-secondary"); + browseButton.setOnAction(ignored -> browseForDestinationParent()); + browseButton.setDisable(moving); + + destinationParentField.setDisable(moving); + destinationNameField.setDisable(moving); + + final HBox destinationParentRow = new HBox(12, destinationParentField, browseButton); + HBox.setHgrow(destinationParentField, Priority.ALWAYS); + + final Label note = new Label(Container.i18n().format( + I18n.ASSETS_RELOCATE_WIZARD_ASSETS_ROOT_HINT, + assetsRoot().toString())); + note.getStyleClass().add("studio-launcher-feedback"); + note.setWrapText(true); + + stepBody.getChildren().setAll( + AssetDetailsUiSupport.createKeyValueRow( + Container.i18n().text(I18n.ASSETS_RELOCATE_WIZARD_LABEL_CURRENT_ROOT), + currentRelativeRoot()), + AssetDetailsUiSupport.createKeyValueRow( + Container.i18n().text(I18n.ASSETS_RELOCATE_WIZARD_LABEL_DESTINATION_PARENT), + destinationParentRow), + AssetDetailsUiSupport.createKeyValueRow( + Container.i18n().text(I18n.ASSETS_RELOCATE_WIZARD_LABEL_DESTINATION_NAME), + destinationNameField), + AssetDetailsUiSupport.createKeyValueRow( + Container.i18n().text(I18n.ASSETS_RELOCATE_WIZARD_LABEL_TARGET_ROOT), + targetRootField), + note); + } + + private void renderSummaryStep() { + stepTitle.textProperty().unbind(); + stepDescription.textProperty().unbind(); + stepTitle.textProperty().bind(Container.i18n().bind(I18n.ASSETS_RELOCATE_WIZARD_STEP_SUMMARY_TITLE)); + stepDescription.textProperty().bind(Container.i18n().bind(I18n.ASSETS_RELOCATE_WIZARD_STEP_SUMMARY_DESCRIPTION)); + stepBody.getChildren().setAll( + AssetDetailsUiSupport.createKeyValueRow( + Container.i18n().text(I18n.ASSETS_RELOCATE_WIZARD_LABEL_CURRENT_ROOT), + currentRelativeRoot()), + AssetDetailsUiSupport.createKeyValueRow( + Container.i18n().text(I18n.ASSETS_RELOCATE_WIZARD_LABEL_DESTINATION_PARENT), + displayDestinationParent()), + AssetDetailsUiSupport.createKeyValueRow( + Container.i18n().text(I18n.ASSETS_RELOCATE_WIZARD_LABEL_DESTINATION_NAME), + normalizedDestinationName()), + AssetDetailsUiSupport.createKeyValueRow( + Container.i18n().text(I18n.ASSETS_RELOCATE_WIZARD_LABEL_TARGET_ROOT), + targetRelativeRoot()), + AssetDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_RELOCATE_WIZARD_NOTE))); + } + + private void renderWaitingStep() { + stepTitle.textProperty().unbind(); + stepDescription.textProperty().unbind(); + stepTitle.textProperty().bind(Container.i18n().bind(I18n.ASSETS_RELOCATE_WIZARD_STEP_WAITING_TITLE)); + stepDescription.textProperty().bind(Container.i18n().bind(I18n.ASSETS_RELOCATE_WIZARD_STEP_WAITING_DESCRIPTION)); + final ProgressIndicator indicator = new ProgressIndicator(); + final VBox box = new VBox(16, indicator, AssetDetailsUiSupport.createSectionMessage(targetRelativeRoot())); + box.setAlignment(Pos.CENTER_LEFT); + stepBody.getChildren().setAll(box); + } + + private void goBack() { + if (moving || stepIndex == 0) { + return; + } + stepIndex -= 1; + renderStep(); + } + + private void goNext() { + if (moving || !validateDestination(true)) { + return; + } + feedbackLabel.setText(""); + stepIndex = 1; + renderStep(); + } + + private void confirmMove() { + if (moving || !validateDestination(true)) { + return; + } + moving = true; + feedbackLabel.setText(""); + stepIndex = 2; + renderStep(); + final MoveAssetRequest request = new MoveAssetRequest( + projectReference.toPackerProjectContext(), + assetReference, + targetRelativeRoot()); + Container.backgroundTasks().submit(() -> moveAsset(request)); + } + + private void moveAsset(MoveAssetRequest request) { + try { + final MoveAssetResult moveResult = Container.packer().workspaceService().moveAsset(request); + Platform.runLater(() -> applyMoveResult(moveResult)); + } catch (RuntimeException exception) { + Platform.runLater(() -> applyMoveFailure(exception)); + } + } + + private void applyMoveResult(MoveAssetResult moveResult) { + moving = false; + if (moveResult.status() == PackerOperationStatus.SUCCESS && moveResult.assetReference() != null) { + result.set(moveResult.assetReference()); + stage.close(); + return; + } + feedbackLabel.setText(Objects.requireNonNullElse(moveResult.summary(), "Unable to move asset.")); + stepIndex = 0; + renderStep(); + } + + private void applyMoveFailure(RuntimeException exception) { + moving = false; + feedbackLabel.setText(exception.getMessage() == null || exception.getMessage().isBlank() + ? "Unable to move asset." + : exception.getMessage()); + stepIndex = 0; + renderStep(); + } + + private void browseForDestinationParent() { + final DirectoryChooser chooser = new DirectoryChooser(); + chooser.setTitle(Container.i18n().text(I18n.ASSETS_RELOCATE_WIZARD_BROWSE_TITLE)); + chooser.setInitialDirectory(existingInitialBrowseDirectory().toFile()); + final File selected = chooser.showDialog(stage); + if (selected == null) { + return; + } + final Path selectedPath = selected.toPath().toAbsolutePath().normalize(); + if (!selectedPath.startsWith(assetsRoot())) { + feedbackLabel.setText(Container.i18n().text(I18n.ASSETS_RELOCATE_WIZARD_ERROR_OUTSIDE_ASSETS)); + return; + } + destinationParentField.setText(relativeDestinationParent(selectedPath)); + targetRootField.setText(displayTargetRoot()); + } + + private boolean validateDestination(boolean showFeedback) { + final String parent = normalizedDestinationParent(); + if (parent == null) { + if (showFeedback) { + feedbackLabel.setText(Container.i18n().text(I18n.ASSETS_RELOCATE_WIZARD_ERROR_PARENT)); + } + return false; + } + final String name = normalizedDestinationName(); + if (name == null) { + if (showFeedback) { + feedbackLabel.setText(Container.i18n().text(I18n.ASSETS_RELOCATE_WIZARD_ERROR_NAME)); + } + return false; + } + final String targetRoot = targetRelativeRoot(); + if (targetRoot == null) { + if (showFeedback) { + feedbackLabel.setText(Container.i18n().text(I18n.ASSETS_RELOCATE_WIZARD_ERROR_OUTSIDE_ASSETS)); + } + return false; + } + if (targetRoot.equals(currentRelativeRoot())) { + if (showFeedback) { + feedbackLabel.setText(Container.i18n().text(I18n.ASSETS_RELOCATE_WIZARD_ERROR_TARGET_SAME)); + } + return false; + } + if (targetManifestPath().toFile().isFile()) { + if (showFeedback) { + feedbackLabel.setText(Container.i18n().text(I18n.ASSETS_RELOCATE_WIZARD_ERROR_TARGET_ALREADY_ASSET)); + } + return false; + } + if (showFeedback) { + feedbackLabel.setText(""); + } + targetRootField.setText(displayTargetRoot()); + return true; + } + + private String initialDestinationParent() { + return relativeDestinationParent(assetSummary.assetRoot().getParent()); + } + + private String relativeDestinationParent(Path parentPath) { + final Path normalized = Objects.requireNonNull(parentPath, "parentPath").toAbsolutePath().normalize(); + if (normalized.equals(assetsRoot())) { + return ""; + } + return assetsRoot().relativize(normalized).toString().replace('\\', '/'); + } + + private Path existingInitialBrowseDirectory() { + final String normalizedParent = normalizedDestinationParent(); + if (normalizedParent == null || normalizedParent.isBlank()) { + return assetsRoot(); + } + final Path candidate = assetsRoot().resolve(normalizedParent).toAbsolutePath().normalize(); + return candidate.startsWith(assetsRoot()) && candidate.toFile().isDirectory() + ? candidate + : assetsRoot(); + } + + private String normalizedDestinationParent() { + final String raw = Objects.requireNonNullElse(destinationParentField.getText(), "").trim().replace('\\', '/'); + if (raw.isBlank()) { + return ""; + } + final Path normalized = Path.of(raw).normalize(); + if (normalized.isAbsolute() || normalized.startsWith("..")) { + return null; + } + return normalized.toString().replace('\\', '/'); + } + + private String normalizedDestinationName() { + final String raw = Objects.requireNonNullElse(destinationNameField.getText(), "").trim(); + if (raw.isBlank()) { + return null; + } + final Path normalized = Path.of(raw).normalize(); + if (normalized.isAbsolute() || normalized.startsWith("..") || normalized.getNameCount() != 1) { + return null; + } + final String value = normalized.toString().replace('\\', '/'); + if (value.isBlank() || value.contains("/")) { + return null; + } + return value; + } + + private String targetRelativeRoot() { + final String parent = normalizedDestinationParent(); + final String name = normalizedDestinationName(); + if (parent == null || name == null) { + return null; + } + return parent.isBlank() ? name : parent + "/" + name; + } + + private String displayDestinationParent() { + final String parent = normalizedDestinationParent(); + return parent == null || parent.isBlank() ? "." : parent; + } + + private String displayTargetRoot() { + final String targetRoot = targetRelativeRoot(); + return targetRoot == null ? "" : targetRoot; + } + + private Path targetManifestPath() { + final String targetRoot = Objects.requireNonNullElse(targetRelativeRoot(), ""); + return assetsRoot().resolve(targetRoot).resolve("asset.json").toAbsolutePath().normalize(); + } + + private String currentRelativeRoot() { + return assetsRoot().relativize(assetSummary.assetRoot()).toString().replace('\\', '/'); + } + + private Path assetsRoot() { + return projectReference.rootPath().resolve("assets").toAbsolutePath().normalize(); + } +} diff --git a/prometeu-studio/src/main/resources/i18n/messages.properties b/prometeu-studio/src/main/resources/i18n/messages.properties index 8265f6ec..9368c8b7 100644 --- a/prometeu-studio/src/main/resources/i18n/messages.properties +++ b/prometeu-studio/src/main/resources/i18n/messages.properties @@ -100,7 +100,7 @@ assets.deleteDialog.note=Only asset.json is deleted. The asset directory and its assets.deleteDialog.confirm=Delete assets.action.includeInBuild=Include In Build assets.action.excludeFromBuild=Exclude From Build -assets.action.relocate=Relocate +assets.action.relocate=Move assets.action.remove=Remove assets.mutation.previewTitle=Preview: {0} assets.mutation.section.changes=Changes @@ -204,6 +204,8 @@ assets.relocateWizard.step.destination.title=Choose Destination assets.relocateWizard.step.destination.description=Pick the parent directory and the new folder name for this asset. assets.relocateWizard.step.summary.title=Review Relocation assets.relocateWizard.step.summary.description=Review the mutation impact before confirming this relocation. +assets.relocateWizard.step.waiting.title=Moving Asset +assets.relocateWizard.step.waiting.description=Wait while the packer applies this move. assets.relocateWizard.label.currentRoot=Current Asset Root assets.relocateWizard.label.destinationParent=Destination Parent assets.relocateWizard.label.destinationName=Destination Folder Name @@ -214,4 +216,9 @@ assets.relocateWizard.browse.title=Choose Destination Parent Directory assets.relocateWizard.note=OK applies this relocation immediately. Use Back if you need to change the destination. assets.relocateWizard.button.confirm=OK assets.relocateWizard.button.preview=Preview Relocation +assets.relocateWizard.error.parent=Destination parent must stay inside assets/. +assets.relocateWizard.error.name=Destination folder name is required. +assets.relocateWizard.error.outsideAssets=The planned target root must stay inside assets/. +assets.relocateWizard.error.targetAlreadyAsset=The planned target root already contains asset.json. +assets.relocateWizard.error.targetSame=The planned target root must differ from the current asset root. workspace.debug=Debug diff --git a/test-projects/main/.studio/activities.json b/test-projects/main/.studio/activities.json index 3a8ff976..cfab2b2c 100644 --- a/test-projects/main/.studio/activities.json +++ b/test-projects/main/.studio/activities.json @@ -3,6 +3,591 @@ "message" : "8 assets loaded", "severity" : "SUCCESS", "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: test", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: bla", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: one-more-atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: ui_atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: one-more-atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: bbb2", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: ui_atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: Bigode", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Asset scan started", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Asset moved: bbb2 -> recovered/bbb2", + "severity" : "SUCCESS", + "sticky" : false +}, { + "source" : "Assets", + "message" : "8 assets loaded", + "severity" : "SUCCESS", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: test", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: bla", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: one-more-atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: ui_atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: one-more-atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: ui_atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: Bigode", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: bbb2", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Asset scan started", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Asset created: bbb2", + "severity" : "SUCCESS", + "sticky" : false +}, { + "source" : "Assets", + "message" : "7 assets loaded", + "severity" : "SUCCESS", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: test", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: bla", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: one-more-atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: ui_atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: one-more-atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: ui_atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: Bigode", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Asset scan started", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Asset moved: recovered/sound -> ui/sound", + "severity" : "SUCCESS", + "sticky" : false +}, { + "source" : "Assets", + "message" : "7 assets loaded", + "severity" : "SUCCESS", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: test", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: one-more-atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: ui_atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: bla", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: one-more-atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: ui_atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: Bigode", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Asset scan started", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "7 assets loaded", + "severity" : "SUCCESS", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: test", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: one-more-atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: ui_atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: bla", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: one-more-atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: ui_atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: Bigode", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Asset scan started", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Asset moved: ui/sound -> recovered/sound", + "severity" : "SUCCESS", + "sticky" : false +}, { + "source" : "Assets", + "message" : "7 assets loaded", + "severity" : "SUCCESS", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: test", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: bla", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: one-more-atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: ui_atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: one-more-atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: ui_atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: Bigode", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Asset scan started", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "7 assets loaded", + "severity" : "SUCCESS", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: test", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: bla", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: one-more-atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: ui_atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: one-more-atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: ui_atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: Bigode", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Asset scan started", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Asset moved: sound -> ui/sound", + "severity" : "SUCCESS", + "sticky" : false +}, { + "source" : "Assets", + "message" : "7 assets loaded", + "severity" : "SUCCESS", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: test", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: one-more-atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: ui_atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: bla", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: one-more-atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: ui_atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: Bigode", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Asset scan started", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "7 assets loaded", + "severity" : "SUCCESS", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: test", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: one-more-atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: ui_atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: bla", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: one-more-atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: ui_atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: Bigode", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Asset scan started", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Asset moved: ui/bigode -> bigode", + "severity" : "SUCCESS", + "sticky" : false +}, { + "source" : "Assets", + "message" : "7 assets loaded", + "severity" : "SUCCESS", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: test", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: one-more-atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: Bigode", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: ui_atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: bla", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: one-more-atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: ui_atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Asset scan started", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "7 assets loaded", + "severity" : "SUCCESS", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: test", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: one-more-atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: Bigode", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: ui_atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: bla", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: one-more-atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: ui_atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Asset scan started", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Asset moved: bigode -> ui/bigode", + "severity" : "SUCCESS", + "sticky" : false +}, { + "source" : "Assets", + "message" : "7 assets loaded", + "severity" : "SUCCESS", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: test", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: one-more-atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: ui_atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: bla", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: one-more-atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: ui_atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: Bigode", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Asset scan started", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "8 assets loaded", + "severity" : "SUCCESS", + "sticky" : false }, { "source" : "Assets", "message" : "Asset scan diagnostics updated.", @@ -1913,354 +2498,4 @@ "message" : "Discovered asset: Novo Asset", "severity" : "INFO", "sticky" : false -}, { - "source" : "Assets", - "message" : "Discovered asset: ui_atlas", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Discovered asset: Bigode", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "8 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Discovered asset: test", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Discovered asset: one-more-atlas", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Discovered asset: ui_atlas", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Discovered asset: bla", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Discovered asset: one-more-atlas", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Discovered asset: Novo Asset", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Discovered asset: ui_atlas", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Discovered asset: Bigode", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "6 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "6 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "6 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "6 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "6 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "6 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "6 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "6 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "6 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "6 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "6 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "6 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "5 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "5 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "5 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "4 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "4 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "4 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "4 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "4 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "4 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "4 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "4 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Studio", - "message" : "Project opened: main", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "4 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "4 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "4 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "4 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "4 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false } ] \ No newline at end of file diff --git a/test-projects/main/assets/.prometeu/index.json b/test-projects/main/assets/.prometeu/index.json index 6a67002d..983b3302 100644 --- a/test-projects/main/assets/.prometeu/index.json +++ b/test-projects/main/assets/.prometeu/index.json @@ -1,6 +1,6 @@ { "schema_version" : 1, - "next_asset_id" : 14, + "next_asset_id" : 15, "assets" : [ { "asset_id" : 3, "asset_uuid" : "21953cb8-4101-4790-9e5e-d95f5fbc9b5a", @@ -14,7 +14,7 @@ }, { "asset_id" : 8, "asset_uuid" : "9a7386e7-6f0e-4e4c-9919-0de71e0b7031", - "root" : "sound", + "root" : "ui/sound", "included_in_build" : true }, { "asset_id" : 9, @@ -36,5 +36,10 @@ "asset_uuid" : "4d9847b0-5a23-421f-8b78-bf3909ca2281", "root" : "recovered/one-more-atlas", "included_in_build" : true + }, { + "asset_id" : 14, + "asset_uuid" : "f64d3bfe-443d-4703-b62a-face19a32cac", + "root" : "recovered/bbb2", + "included_in_build" : true } ] } \ No newline at end of file diff --git a/test-projects/main/assets/recovered/bbb2/asset.json b/test-projects/main/assets/recovered/bbb2/asset.json new file mode 100644 index 00000000..7adf455d --- /dev/null +++ b/test-projects/main/assets/recovered/bbb2/asset.json @@ -0,0 +1,14 @@ +{ + "schema_version" : 1, + "asset_uuid" : "f64d3bfe-443d-4703-b62a-face19a32cac", + "name" : "bbb2", + "type" : "image_bank", + "inputs" : { }, + "output" : { + "codec" : "NONE", + "format" : "TILES/indexed_v1" + }, + "preload" : { + "enabled" : false + } +} \ No newline at end of file diff --git a/test-projects/main/assets/sound/asset.json b/test-projects/main/assets/ui/sound/asset.json similarity index 100% rename from test-projects/main/assets/sound/asset.json rename to test-projects/main/assets/ui/sound/asset.json