added move action into asset workspace

This commit is contained in:
bQUARKz 2026-03-16 09:15:40 +00:00
parent 29a2398b1d
commit bf4dc17469
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
24 changed files with 1651 additions and 387 deletions

View File

@ -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/**`

View File

@ -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`

View File

@ -15,5 +15,7 @@ public interface PackerWorkspaceService {
RegisterAssetResult registerAsset(RegisterAssetRequest request);
MoveAssetResult moveAsset(MoveAssetRequest request);
DeleteAssetResult deleteAsset(DeleteAssetRequest request);
}

View File

@ -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();
}
}

View File

@ -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");
}
}
}

View File

@ -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();
}
}
}

View File

@ -2,5 +2,6 @@ package p.packer.messages.assets;
public enum AssetAction {
REGISTER,
MOVE,
DELETE
}

View File

@ -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<PackerDiagnostic> diagnostics,
boolean canMove,
String reason,
Optional<String> targetRelativeRoot,
Optional<Path> targetRoot,
Optional<Path> 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");
}
}

View File

@ -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<PackerRegistryEntry> 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<String> 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<String, Object> 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<PackerRegistryEntry> updatedEntries = registry.assets().stream()
.map(candidate -> candidate.assetId() == updatedEntry.assetId() ? updatedEntry : candidate)
.toList();
return registry.withAssets(updatedEntries, registry.nextAssetId());
}
}

View File

@ -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<PackerAssetActionAvailability> 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<PackerDiagnostic> 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<PackerDiagnostic> diagnostics, String fallback) {
return diagnostics.stream()
.filter(PackerDiagnostic::blocking)
@ -162,11 +268,25 @@ public final class PackerAssetActionReadService {
.orElse(fallback);
}
private List<PackerDiagnostic> combinedDiagnostics(
List<PackerDiagnostic> left,
List<PackerDiagnostic> right) {
final List<PackerDiagnostic> combined = new ArrayList<>(left);
combined.addAll(right);
@SafeVarargs
private List<PackerDiagnostic> combinedDiagnostics(List<PackerDiagnostic>... groups) {
final List<PackerDiagnostic> combined = new ArrayList<>();
for (List<PackerDiagnostic> 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;
}
}

View File

@ -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<PackerRegistryEntry> updatedRegistryEntry,
Path sourceRoot,
Path targetRoot,
Path targetManifestPath) {
final PackerAssetDeclarationParseResult parsed = declarationParser.parse(targetManifestPath);
final List<PackerRuntimeAsset> 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);
}
}

View File

@ -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"));

View File

@ -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"),

View File

@ -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));
});
}

View File

@ -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.");

View File

@ -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);
};
}

View File

@ -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() {

View File

@ -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");
}

View File

@ -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<AssetReference> 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<AssetReference> 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();
}
}

View File

@ -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

View File

@ -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
} ]

View File

@ -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
} ]
}

View File

@ -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
}
}