added asset workspace working with packer
This commit is contained in:
parent
1f6df50f09
commit
ebbfe311ee
@ -404,27 +404,20 @@ Variants:
|
||||
* `add --dir` creates a dedicated asset root dir
|
||||
* `add --in-place` anchors next to the file
|
||||
|
||||
### 13.3 `prometeu packer adopt`
|
||||
|
||||
Scans workspace for unregistered `asset.json` anchors and offers to register them.
|
||||
|
||||
* default: dry-run list
|
||||
* `--apply` registers them
|
||||
|
||||
### 13.4 `prometeu packer forget <name|id|uuid>`
|
||||
### 13.3 `prometeu packer forget <name|id|uuid>`
|
||||
|
||||
Removes an asset from the registry without deleting files.
|
||||
|
||||
Useful for WIP and cleanup.
|
||||
|
||||
### 13.5 `prometeu packer rm <name|id|uuid> [--delete]`
|
||||
### 13.4 `prometeu packer rm <name|id|uuid> [--delete]`
|
||||
|
||||
Removes the asset from the registry.
|
||||
|
||||
* default: no deletion
|
||||
* `--delete` can remove the asset root dir (dangerous; must confirm in UI tooling, or require a force flag in CLI)
|
||||
|
||||
### 13.6 `prometeu packer list`
|
||||
### 13.5 `prometeu packer list`
|
||||
|
||||
Lists managed assets:
|
||||
|
||||
|
||||
@ -26,7 +26,7 @@ There are two different truths for two different jobs:
|
||||
- each `asset.json` tells the packer what that asset declares locally.
|
||||
|
||||
This split matters.
|
||||
Without it, copy/move/adopt flows become ambiguous very quickly.
|
||||
Without it, copy/move/register flows become ambiguous very quickly.
|
||||
|
||||
## 3. What is the difference between `asset_id` and `asset_name`?
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ Runtime-side reading semantics for `assets.pa` are defined upstream in `../runti
|
||||
|
||||
- project workspace with `assets/`
|
||||
- packer registry and control data under `assets/.prometeu/`
|
||||
- managed asset roots anchored by `asset.json`
|
||||
- registered asset roots anchored by `asset.json`
|
||||
|
||||
## Core Rules
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Workspace, Registry, and Asset Identity Specification
|
||||
|
||||
Status: Draft
|
||||
Scope: Managed asset roots, registry authority, and stable identity
|
||||
Scope: Registered asset roots, registry authority, and stable identity
|
||||
Purpose: Define how the packer recognizes, tracks, and preserves asset identity.
|
||||
|
||||
## Authority and Precedence
|
||||
@ -16,18 +16,22 @@ This specification consolidates the initial packer agenda and decision wave into
|
||||
|
||||
## Core Rules
|
||||
|
||||
1. One managed asset equals one asset root directory.
|
||||
1. One registered asset equals one asset root directory.
|
||||
2. One asset root contains exactly one anchor `asset.json`.
|
||||
3. `assets/.prometeu/index.json` is the authoritative registry of managed assets.
|
||||
3. `assets/.prometeu/index.json` is the authoritative registry of registered assets.
|
||||
4. `asset.json` is the authoritative asset-local declaration.
|
||||
5. An asset root absent from the registry is not part of the managed build set.
|
||||
5. An asset root absent from the registry is `unregistered`.
|
||||
6. `unregistered` assets are always `excluded` from build participation.
|
||||
7. Registered assets may be `included` or `excluded` from build participation without losing identity.
|
||||
8. The baseline build set includes registered assets whose registry entry is marked as build-included.
|
||||
|
||||
## Identity Model
|
||||
|
||||
Each managed asset has:
|
||||
Each registered asset has:
|
||||
|
||||
- `asset_id`: stable project-local identity
|
||||
- `asset_uuid`: stable long-lived identity for migration/tooling scenarios
|
||||
- `included_in_build`: build participation flag persisted in the registry
|
||||
|
||||
The following are not primary identity:
|
||||
|
||||
@ -50,15 +54,26 @@ Rules:
|
||||
|
||||
Renaming `asset_name` is an API-visible change, but not an identity change.
|
||||
|
||||
## Orphan Anchors
|
||||
## Unregistered Roots
|
||||
|
||||
An `asset.json` without a corresponding registry entry is an orphan declaration.
|
||||
An `asset.json` without a corresponding registry entry is an unregistered asset root.
|
||||
|
||||
Rules:
|
||||
|
||||
- it does not enter the build automatically;
|
||||
- it is excluded from the build automatically;
|
||||
- it is diagnosable;
|
||||
- it is adoptable only through explicit flow.
|
||||
- it becomes registered only through explicit flow.
|
||||
|
||||
## Build Participation
|
||||
|
||||
Build participation is registry-owned for registered assets.
|
||||
|
||||
Rules:
|
||||
|
||||
- excluding a registered asset from builds does not remove `asset_id`;
|
||||
- excluding a registered asset from builds does not remove `asset_uuid`;
|
||||
- inclusion changes must preserve asset identity and registry location;
|
||||
- unregistered assets cannot be marked as build-included.
|
||||
|
||||
## Asset ID Allocation
|
||||
|
||||
@ -77,7 +92,7 @@ Identity-bearing conflicts are structural errors.
|
||||
|
||||
Examples:
|
||||
|
||||
- duplicate or ambiguous anchors under managed expectations;
|
||||
- duplicate or ambiguous anchors under registered expectations;
|
||||
- manual copy that creates identity collision;
|
||||
- registered root missing anchor.
|
||||
|
||||
@ -91,6 +106,6 @@ Examples:
|
||||
|
||||
This specification is complete enough when:
|
||||
|
||||
- managed asset boundaries are unambiguous;
|
||||
- registered asset boundaries are unambiguous;
|
||||
- registry authority is explicit;
|
||||
- identity survives relocation without ambiguity.
|
||||
|
||||
@ -92,7 +92,7 @@ Rules:
|
||||
|
||||
## Preload
|
||||
|
||||
Each managed asset must declare preload intent explicitly.
|
||||
Each registered asset must declare preload intent explicitly.
|
||||
|
||||
Baseline shape:
|
||||
|
||||
|
||||
@ -74,7 +74,7 @@ The header carries:
|
||||
|
||||
Rules:
|
||||
|
||||
- one entry per managed asset in the current build set;
|
||||
- one entry per included asset in the current build set;
|
||||
- no secondary runtime-only identity layer;
|
||||
- no synthetic dense reindexing layer;
|
||||
- `asset_name` remains present as logical/API-facing metadata.
|
||||
|
||||
@ -12,12 +12,12 @@ This specification consolidates the initial packer agenda and decision wave into
|
||||
|
||||
Diagnostics are divided into:
|
||||
|
||||
- structural managed-world errors;
|
||||
- structural registered-world errors;
|
||||
- advisory workspace hygiene diagnostics.
|
||||
|
||||
Rules:
|
||||
|
||||
- structural managed-world errors block builds;
|
||||
- structural registered-world errors block builds;
|
||||
- hygiene findings do not block baseline builds by default;
|
||||
- workspace scanning is broader than build validation.
|
||||
|
||||
@ -25,7 +25,7 @@ Rules:
|
||||
|
||||
Baseline doctor behavior:
|
||||
|
||||
- managed-world validation by default;
|
||||
- registered-world validation by default;
|
||||
- broader workspace hygiene scanning in expanded workspace mode;
|
||||
- safe mechanical fix application only for baseline fix flows.
|
||||
|
||||
@ -38,7 +38,7 @@ Rules:
|
||||
- destructive or relocational mutations require explicit consent;
|
||||
- quarantine is explicit and reversible;
|
||||
- duplicate content detection is advisory, not structurally invalid by itself;
|
||||
- orphan anchors are diagnosable, not build-active.
|
||||
- unregistered asset roots are diagnosable and remain excluded from builds.
|
||||
|
||||
## Studio-First Service Surface
|
||||
|
||||
@ -48,8 +48,8 @@ Baseline core services:
|
||||
|
||||
- `init_workspace`
|
||||
- `register_asset`
|
||||
- `adopt_asset`
|
||||
- `forget_asset`
|
||||
- `include_asset_in_build`
|
||||
- `exclude_asset_from_build`
|
||||
- `remove_asset`
|
||||
- `list_assets`
|
||||
- `get_asset_details`
|
||||
@ -92,7 +92,7 @@ Rules:
|
||||
|
||||
- preview or analysis precedes broad mutation where appropriate;
|
||||
- safe-fix flows are modeled as services, not merely terminal flags;
|
||||
- `forget_asset` and `remove_asset` remain distinct operations.
|
||||
- `exclude_asset_from_build` and `remove_asset` remain distinct operations.
|
||||
|
||||
## Structured Responses
|
||||
|
||||
|
||||
@ -45,8 +45,8 @@ The `Assets` workspace is a Studio view over packer services.
|
||||
|
||||
The workspace must assume:
|
||||
|
||||
- managed assets are the primary unit of identity and operation;
|
||||
- orphan declarations may exist and must remain visible;
|
||||
- registered and unregistered assets may coexist inside `assets/`;
|
||||
- unregistered assets remain visible but excluded from builds;
|
||||
- assets may aggregate many internal inputs;
|
||||
- runtime-facing output contract data exists for each asset;
|
||||
- diagnostics, activity, progress, and staged mutation responses are available from Studio-facing services.
|
||||
@ -62,7 +62,7 @@ The `Assets` workspace is:
|
||||
|
||||
The workspace must help the user understand:
|
||||
|
||||
- what the managed asset is;
|
||||
- what the asset registration and build status are;
|
||||
- where the asset root lives;
|
||||
- which internal inputs belong to that asset;
|
||||
- what the asset declares toward the runtime-facing contract;
|
||||
@ -93,7 +93,8 @@ Filesystem structure may be visible and actionable as supporting context, but it
|
||||
- The left navigation surface must be a custom `Asset Tree` / `Asset Navigator`.
|
||||
- Asset nodes must expose strong visual semantics through icons, badges, and state styling.
|
||||
- Asset nodes must surface:
|
||||
- managed/orphan state,
|
||||
- registration status,
|
||||
- build participation,
|
||||
- diagnostics presence,
|
||||
- preload intent,
|
||||
- and broad asset family or type where available.
|
||||
@ -102,8 +103,8 @@ Filesystem structure may be visible and actionable as supporting context, but it
|
||||
### Filters and Search
|
||||
|
||||
- The baseline filters are:
|
||||
- `Managed`
|
||||
- `Orphan`
|
||||
- `Registered`
|
||||
- `Unregistered`
|
||||
- `Diagnostics`
|
||||
- `Preload`
|
||||
- The navigator must support textual search.
|
||||
@ -112,11 +113,22 @@ Filesystem structure may be visible and actionable as supporting context, but it
|
||||
- asset root path
|
||||
- path context
|
||||
|
||||
### Workspace Actions
|
||||
|
||||
- Workspace-scoped actions must appear above the main asset workspace split, not inside the navigator list region.
|
||||
- Workspace-scoped actions must not be mixed into the selected-asset details surface.
|
||||
- Workspace actions should remain visible without requiring a selected asset.
|
||||
- Global asset-manager actions such as `Add Asset`, `Doctor`, and `Pack` belong to this workspace-level action area.
|
||||
|
||||
### Navigator Actions
|
||||
|
||||
- Navigator-local controls may include search and filters that are specifically about asset discovery and navigation.
|
||||
|
||||
### Selection Rules
|
||||
|
||||
- The baseline navigator is single-select.
|
||||
- Managed asset selection must be preserved by `asset_id`.
|
||||
- Orphan selection must be preserved by asset root path until the asset becomes managed.
|
||||
- Registered asset selection must be preserved by `asset_id`.
|
||||
- Unregistered selection must be preserved by asset root path until the asset becomes registered.
|
||||
- If an asset changes state while preserving identity, selection must remain attached to it.
|
||||
- If the selected asset is removed from the navigator, selection must clear explicitly.
|
||||
- Selection must never silently drift to another asset due to refresh ordering.
|
||||
@ -127,8 +139,8 @@ Filesystem structure may be visible and actionable as supporting context, but it
|
||||
- The navigator must define explicit `no assets` state.
|
||||
- The navigator must define explicit `no results` state.
|
||||
- The navigator must define explicit `workspace error` state.
|
||||
- Orphan assets must appear in the same navigator flow as managed assets.
|
||||
- Orphan styling must communicate `declared, not managed`, not `broken`.
|
||||
- Unregistered assets must appear in the same navigator flow as registered assets.
|
||||
- Unregistered styling must communicate `present but not yet registered`, not `broken`.
|
||||
|
||||
### Inputs Expansion
|
||||
|
||||
@ -137,22 +149,22 @@ Filesystem structure may be visible and actionable as supporting context, but it
|
||||
|
||||
## Selected Asset Details Rules
|
||||
|
||||
The selected asset view must use this fixed section order:
|
||||
The selected asset view must use this stable composition:
|
||||
|
||||
1. `Summary`
|
||||
1. top row with `Summary` and `Actions` side by side
|
||||
2. `Runtime Contract`
|
||||
3. `Inputs / Preview`
|
||||
4. `Diagnostics`
|
||||
5. `Actions`
|
||||
|
||||
This section order is stable across asset families.
|
||||
This composition is stable across asset families.
|
||||
|
||||
### Summary
|
||||
|
||||
- `Summary` must always be present for a selected asset.
|
||||
- `Summary` must show:
|
||||
- `asset_name`
|
||||
- managed/orphan state
|
||||
- registration status
|
||||
- build participation
|
||||
- `asset_id` when available
|
||||
- family/type
|
||||
- asset root path
|
||||
@ -187,16 +199,21 @@ This section order is stable across asset families.
|
||||
### Actions
|
||||
|
||||
- Actions must be explicit.
|
||||
- Actions that target the selected asset must appear beside `Summary`, not in the navigator area.
|
||||
- Safe and routine actions must be visually separated from sensitive mutations.
|
||||
- Hidden or automatic repair behavior is not allowed in the selected-asset view.
|
||||
|
||||
## Action Rules
|
||||
|
||||
- Primary actions for managed assets include `Doctor` and `Build`.
|
||||
- Primary actions for orphan assets include `Adopt`.
|
||||
- `Register` must remain available when explicit registration is the correct flow.
|
||||
- Sensitive actions such as `Forget`, `Remove`, and quarantine-like actions must be visually separated from routine actions.
|
||||
- Registered assets excluded from builds must expose an explicit `Include In Build` action.
|
||||
- Primary actions for unregistered assets include `Register`.
|
||||
- `Register` is a direct action in Studio UX. If registration is valid, clicking it must register immediately without an extra acknowledgement step.
|
||||
- If registration is blocked, Studio may surface the blocking preview inline so the user can inspect why registration cannot proceed.
|
||||
- Sensitive actions such as `Exclude From Build`, `Remove`, and `Relocate` must be visually separated from routine actions.
|
||||
- Sensitive actions must not look interchangeable with routine actions.
|
||||
- `Relocate` must collect an explicit user-chosen destination root before the staged preview is generated.
|
||||
- Automatic relocation targets are not sufficient Studio UX for `Relocate`.
|
||||
- `Exclude From Build` and `Remove` must confirm in a modal review surface rather than an inline staged panel.
|
||||
|
||||
## Activity, Progress, and Logs Rules
|
||||
|
||||
@ -230,24 +247,25 @@ This section order is stable across asset families.
|
||||
|
||||
Sensitive asset mutations are `preview-first`.
|
||||
|
||||
The default review surface is an inline staged panel inside the workspace.
|
||||
The review surface depends on the mutation class.
|
||||
|
||||
Modal confirmation is reserved for high-risk final commits such as destructive delete or major relocation.
|
||||
Inline staged panels are allowed for non-destructive blocked or inspect-only flows.
|
||||
|
||||
Modal review and confirmation is required for `Exclude From Build`, `Remove`, and relocation commits.
|
||||
|
||||
### Mutations Requiring Preview
|
||||
|
||||
- `Adopt`
|
||||
- `Forget`
|
||||
- `Exclude From Build`
|
||||
- `Remove`
|
||||
- `Quarantine`
|
||||
- relocational changes such as move or rename of asset roots
|
||||
- batch operations
|
||||
|
||||
`Doctor` and `Build` remain outside this sensitive mutation staging flow.
|
||||
Navigator-level `Doctor` and `Pack`, plus asset-level `Include In Build` and successful `Register`, remain outside this sensitive mutation staging flow.
|
||||
|
||||
### Preview Content
|
||||
|
||||
- Preview must show affected assets.
|
||||
- Relocation preview must show the planned target root chosen by the user.
|
||||
- Preview must show proposed actions.
|
||||
- Preview must distinguish registry impact from workspace impact.
|
||||
- Preview must show blockers, warnings, and safe fixes as separate visual sections.
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package p.packer.api.assets;
|
||||
|
||||
public enum PackerAssetState {
|
||||
MANAGED,
|
||||
ORPHAN,
|
||||
INVALID
|
||||
REGISTERED,
|
||||
UNREGISTERED
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import java.util.Objects;
|
||||
public record PackerAssetSummary(
|
||||
PackerAssetIdentity identity,
|
||||
PackerAssetState state,
|
||||
PackerBuildParticipation buildParticipation,
|
||||
String assetFamily,
|
||||
boolean preloadEnabled,
|
||||
boolean hasDiagnostics) {
|
||||
@ -12,9 +13,16 @@ public record PackerAssetSummary(
|
||||
public PackerAssetSummary {
|
||||
Objects.requireNonNull(identity, "identity");
|
||||
Objects.requireNonNull(state, "state");
|
||||
Objects.requireNonNull(buildParticipation, "buildParticipation");
|
||||
assetFamily = Objects.requireNonNullElse(assetFamily, "unknown").trim();
|
||||
if (assetFamily.isBlank()) {
|
||||
assetFamily = "unknown";
|
||||
}
|
||||
if (state == PackerAssetState.REGISTERED && identity.assetId() == null) {
|
||||
throw new IllegalArgumentException("registered asset must expose assetId");
|
||||
}
|
||||
if (state == PackerAssetState.UNREGISTERED && buildParticipation != PackerBuildParticipation.EXCLUDED) {
|
||||
throw new IllegalArgumentException("unregistered asset must stay excluded from build participation");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
package p.packer.api.assets;
|
||||
|
||||
public enum PackerBuildParticipation {
|
||||
INCLUDED,
|
||||
EXCLUDED
|
||||
}
|
||||
@ -2,9 +2,8 @@ package p.packer.api.mutations;
|
||||
|
||||
public enum PackerMutationType {
|
||||
REGISTER_ASSET,
|
||||
ADOPT_ASSET,
|
||||
FORGET_ASSET,
|
||||
INCLUDE_ASSET_IN_BUILD,
|
||||
EXCLUDE_ASSET_FROM_BUILD,
|
||||
REMOVE_ASSET,
|
||||
QUARANTINE_ASSET,
|
||||
RELOCATE_ASSET
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package p.packer.building;
|
||||
|
||||
import p.packer.api.PackerOperationStatus;
|
||||
import p.packer.api.PackerProjectContext;
|
||||
import p.packer.api.assets.PackerBuildParticipation;
|
||||
import p.packer.api.assets.PackerAssetState;
|
||||
import p.packer.api.diagnostics.PackerDiagnostic;
|
||||
import p.packer.api.diagnostics.PackerDiagnosticCategory;
|
||||
@ -45,16 +46,17 @@ public final class PackerBuildPlanner {
|
||||
final List<PackerPlannedAsset> plannedAssets = new ArrayList<>();
|
||||
|
||||
snapshot.assets().stream()
|
||||
.filter(asset -> asset.state() == PackerAssetState.MANAGED)
|
||||
.filter(asset -> asset.buildParticipation() == PackerBuildParticipation.INCLUDED)
|
||||
.sorted(Comparator.comparingInt(asset -> asset.identity().assetId()))
|
||||
.forEach(asset -> {
|
||||
final var detailsResult = detailsService.getAssetDetails(new GetAssetDetailsRequest(project, Integer.toString(asset.identity().assetId())));
|
||||
diagnostics.addAll(detailsResult.diagnostics());
|
||||
if (detailsResult.details().summary().state() != PackerAssetState.MANAGED) {
|
||||
if (detailsResult.details().summary().state() != PackerAssetState.REGISTERED
|
||||
|| detailsResult.details().summary().buildParticipation() != PackerBuildParticipation.INCLUDED) {
|
||||
diagnostics.add(new PackerDiagnostic(
|
||||
PackerDiagnosticSeverity.ERROR,
|
||||
PackerDiagnosticCategory.BUILD,
|
||||
"Managed asset is not build-eligible: " + asset.identity().assetName(),
|
||||
"Registered asset is not build-eligible: " + asset.identity().assetName(),
|
||||
asset.identity().assetRoot(),
|
||||
true));
|
||||
return;
|
||||
@ -115,7 +117,7 @@ public final class PackerBuildPlanner {
|
||||
"inputs", plannedAssets.stream().map(this::cacheKeyView).toList())));
|
||||
return new PackerBuildPlanResult(
|
||||
diagnostics.isEmpty() ? PackerOperationStatus.SUCCESS : PackerOperationStatus.PARTIAL,
|
||||
"Build plan ready for " + plannedAssets.size() + " managed assets.",
|
||||
"Build plan ready for " + plannedAssets.size() + " included assets.",
|
||||
new PackerBuildPlan(cacheKey, plannedAssets, assetTableJson, preloadJson),
|
||||
List.copyOf(diagnostics));
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import p.packer.api.assets.PackerAssetDetails;
|
||||
import p.packer.api.assets.PackerAssetIdentity;
|
||||
import p.packer.api.assets.PackerAssetState;
|
||||
import p.packer.api.assets.PackerAssetSummary;
|
||||
import p.packer.api.assets.PackerBuildParticipation;
|
||||
import p.packer.api.diagnostics.PackerDiagnostic;
|
||||
import p.packer.api.diagnostics.PackerDiagnosticCategory;
|
||||
import p.packer.api.diagnostics.PackerDiagnosticSeverity;
|
||||
@ -65,13 +66,19 @@ public final class PackerAssetDetailsService {
|
||||
}
|
||||
|
||||
final PackerAssetDeclaration declaration = parsed.declaration();
|
||||
final PackerAssetState state = resolved.registryEntry().isPresent()
|
||||
? PackerAssetState.REGISTERED
|
||||
: PackerAssetState.UNREGISTERED;
|
||||
final PackerAssetSummary summary = new PackerAssetSummary(
|
||||
new PackerAssetIdentity(
|
||||
resolved.registryEntry().map(PackerRegistryEntry::assetId).orElse(null),
|
||||
resolved.registryEntry().map(PackerRegistryEntry::assetUuid).orElse(null),
|
||||
declaration.name(),
|
||||
resolved.assetRoot()),
|
||||
resolved.registryEntry().isPresent() ? PackerAssetState.MANAGED : PackerAssetState.ORPHAN,
|
||||
state,
|
||||
resolved.registryEntry().map(entry -> entry.includedInBuild()
|
||||
? PackerBuildParticipation.INCLUDED
|
||||
: PackerBuildParticipation.EXCLUDED).orElse(PackerBuildParticipation.EXCLUDED),
|
||||
declaration.type(),
|
||||
declaration.preloadEnabled(),
|
||||
!diagnostics.isEmpty());
|
||||
@ -89,13 +96,19 @@ public final class PackerAssetDetailsService {
|
||||
}
|
||||
|
||||
private GetAssetDetailsResult failureResult(ResolvedAssetReference resolved, List<PackerDiagnostic> diagnostics) {
|
||||
final PackerAssetState state = resolved.registryEntry().isPresent()
|
||||
? PackerAssetState.REGISTERED
|
||||
: PackerAssetState.UNREGISTERED;
|
||||
final PackerAssetSummary summary = new PackerAssetSummary(
|
||||
new PackerAssetIdentity(
|
||||
resolved.registryEntry().map(PackerRegistryEntry::assetId).orElse(null),
|
||||
resolved.registryEntry().map(PackerRegistryEntry::assetUuid).orElse(null),
|
||||
resolved.assetRoot().getFileName().toString(),
|
||||
resolved.assetRoot()),
|
||||
PackerAssetState.INVALID,
|
||||
state,
|
||||
resolved.registryEntry().map(entry -> entry.includedInBuild()
|
||||
? PackerBuildParticipation.INCLUDED
|
||||
: PackerBuildParticipation.EXCLUDED).orElse(PackerBuildParticipation.EXCLUDED),
|
||||
"unknown",
|
||||
false,
|
||||
true);
|
||||
|
||||
@ -86,38 +86,29 @@ public final class FileSystemPackerDoctorService implements PackerDoctorService
|
||||
addDiagnostic(diagnostics, seenDiagnostics, diagnostic);
|
||||
}
|
||||
|
||||
if (doctorRequest.mode() == PackerDoctorMode.EXPANDED_WORKSPACE && asset.state() == PackerAssetState.ORPHAN) {
|
||||
if (doctorRequest.mode() == PackerDoctorMode.EXPANDED_WORKSPACE && asset.state() == PackerAssetState.UNREGISTERED) {
|
||||
addDiagnostic(diagnostics, seenDiagnostics, new PackerDiagnostic(
|
||||
PackerDiagnosticSeverity.WARNING,
|
||||
PackerDiagnosticCategory.HYGIENE,
|
||||
"Orphan asset is valid but not registered in the managed build set: " + relativeAssetRoot(project, asset.identity().assetRoot()),
|
||||
"Asset is present in assets/ but not registered, so it stays excluded from builds: " + relativeAssetRoot(project, asset.identity().assetRoot()),
|
||||
asset.identity().assetRoot(),
|
||||
false));
|
||||
if (doctorRequest.includeSafeFixes() && details.summary().state() != PackerAssetState.INVALID) {
|
||||
if (doctorRequest.includeSafeFixes() && details.diagnostics().stream().noneMatch(PackerDiagnostic::blocking)) {
|
||||
safeFixes.add("register_asset " + relativeAssetRoot(project, asset.identity().assetRoot()));
|
||||
}
|
||||
}
|
||||
|
||||
if (doctorRequest.mode() == PackerDoctorMode.EXPANDED_WORKSPACE && isInsideQuarantine(project, asset.identity().assetRoot())) {
|
||||
addDiagnostic(diagnostics, seenDiagnostics, new PackerDiagnostic(
|
||||
PackerDiagnosticSeverity.INFO,
|
||||
PackerDiagnosticCategory.HYGIENE,
|
||||
"Asset root is currently quarantined and excluded from the active workspace surface.",
|
||||
asset.identity().assetRoot(),
|
||||
false));
|
||||
}
|
||||
|
||||
details.inputsByRole().forEach((role, inputs) -> inputs.forEach(input -> {
|
||||
if (Files.isRegularFile(input)) {
|
||||
return;
|
||||
}
|
||||
final boolean managed = asset.state() == PackerAssetState.MANAGED;
|
||||
final boolean registered = asset.state() == PackerAssetState.REGISTERED;
|
||||
addDiagnostic(diagnostics, seenDiagnostics, new PackerDiagnostic(
|
||||
managed ? PackerDiagnosticSeverity.ERROR : PackerDiagnosticSeverity.WARNING,
|
||||
managed ? PackerDiagnosticCategory.STRUCTURAL : PackerDiagnosticCategory.HYGIENE,
|
||||
registered ? PackerDiagnosticSeverity.ERROR : PackerDiagnosticSeverity.WARNING,
|
||||
registered ? PackerDiagnosticCategory.STRUCTURAL : PackerDiagnosticCategory.HYGIENE,
|
||||
"Declared input is missing for role '" + role + "': " + relativeEvidence(project, input),
|
||||
input,
|
||||
managed));
|
||||
registered));
|
||||
}));
|
||||
events.emit(
|
||||
PackerEventKind.PROGRESS_UPDATED,
|
||||
@ -143,7 +134,7 @@ public final class FileSystemPackerDoctorService implements PackerDoctorService
|
||||
if (mode == PackerDoctorMode.EXPANDED_WORKSPACE) {
|
||||
return true;
|
||||
}
|
||||
return state == PackerAssetState.MANAGED || state == PackerAssetState.INVALID;
|
||||
return state == PackerAssetState.REGISTERED;
|
||||
}
|
||||
|
||||
private void addDiagnostic(List<PackerDiagnostic> diagnostics, Set<String> seenDiagnostics, PackerDiagnostic diagnostic) {
|
||||
@ -153,11 +144,6 @@ public final class FileSystemPackerDoctorService implements PackerDoctorService
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInsideQuarantine(PackerProjectContext project, Path assetRoot) {
|
||||
return assetRoot.toAbsolutePath().normalize()
|
||||
.startsWith(project.rootPath().resolve("assets/.prometeu/quarantine").toAbsolutePath().normalize());
|
||||
}
|
||||
|
||||
private String relativeAssetRoot(PackerProjectContext project, Path assetRoot) {
|
||||
return project.rootPath().resolve("assets").toAbsolutePath().normalize()
|
||||
.relativize(assetRoot.toAbsolutePath().normalize())
|
||||
|
||||
@ -37,7 +37,11 @@ public final class FileSystemPackerRegistryRepository implements PackerRegistryR
|
||||
if (asset == null) {
|
||||
continue;
|
||||
}
|
||||
entries.add(new PackerRegistryEntry(asset.assetId, asset.assetUuid, normalizeRoot(asset.root)));
|
||||
entries.add(new PackerRegistryEntry(
|
||||
asset.assetId,
|
||||
asset.assetUuid,
|
||||
normalizeRoot(asset.root),
|
||||
asset.includedInBuild == null || asset.includedInBuild));
|
||||
}
|
||||
}
|
||||
validateEntries(entries);
|
||||
@ -64,7 +68,11 @@ public final class FileSystemPackerRegistryRepository implements PackerRegistryR
|
||||
document.schemaVersion = state.schemaVersion();
|
||||
document.nextAssetId = state.nextAssetId();
|
||||
document.assets = state.assets().stream()
|
||||
.map(entry -> new RegistryAssetDocument(entry.assetId(), entry.assetUuid(), entry.root()))
|
||||
.map(entry -> new RegistryAssetDocument(
|
||||
entry.assetId(),
|
||||
entry.assetUuid(),
|
||||
entry.root(),
|
||||
entry.includedInBuild()))
|
||||
.toList();
|
||||
MAPPER.writerWithDefaultPrettyPrinter().writeValue(registryPath.toFile(), document);
|
||||
} catch (IOException exception) {
|
||||
@ -121,6 +129,7 @@ public final class FileSystemPackerRegistryRepository implements PackerRegistryR
|
||||
private record RegistryAssetDocument(
|
||||
@JsonProperty("asset_id") int assetId,
|
||||
@JsonProperty("asset_uuid") String assetUuid,
|
||||
@JsonProperty("root") String root) {
|
||||
@JsonProperty("root") String root,
|
||||
@JsonProperty("included_in_build") Boolean includedInBuild) {
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,12 @@ import java.util.Objects;
|
||||
public record PackerRegistryEntry(
|
||||
int assetId,
|
||||
String assetUuid,
|
||||
String root) {
|
||||
String root,
|
||||
boolean includedInBuild) {
|
||||
|
||||
public PackerRegistryEntry(int assetId, String assetUuid, String root) {
|
||||
this(assetId, assetUuid, root, true);
|
||||
}
|
||||
|
||||
public PackerRegistryEntry {
|
||||
assetUuid = Objects.requireNonNull(assetUuid, "assetUuid").trim();
|
||||
|
||||
@ -36,9 +36,6 @@ import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class FileSystemPackerMutationService implements PackerMutationService {
|
||||
private static final String RECOVERED_DIR = "recovered";
|
||||
private static final String QUARANTINE_DIR = "quarantine";
|
||||
|
||||
private final PackerWorkspaceFoundation workspaceFoundation;
|
||||
private final PackerAssetDetailsService detailsService;
|
||||
private final PackerProjectWriteCoordinator writeCoordinator;
|
||||
@ -97,10 +94,10 @@ public final class FileSystemPackerMutationService implements PackerMutationServ
|
||||
final Path assetRoot = context.assetDetails().summary().identity().assetRoot();
|
||||
|
||||
return switch (preview.request().type()) {
|
||||
case REGISTER_ASSET, ADOPT_ASSET -> applyRegister(project, registry, assetRoot, preview);
|
||||
case FORGET_ASSET -> applyForget(project, registry, assetRoot, preview);
|
||||
case REGISTER_ASSET -> applyRegister(project, registry, assetRoot, preview);
|
||||
case INCLUDE_ASSET_IN_BUILD -> applyBuildParticipationChange(project, registry, assetRoot, preview, true);
|
||||
case EXCLUDE_ASSET_FROM_BUILD -> applyBuildParticipationChange(project, registry, assetRoot, preview, false);
|
||||
case REMOVE_ASSET -> applyRemove(project, registry, assetRoot, preview);
|
||||
case QUARANTINE_ASSET -> applyQuarantine(project, registry, assetRoot, preview);
|
||||
case RELOCATE_ASSET -> applyRelocate(project, registry, assetRoot, preview);
|
||||
};
|
||||
}
|
||||
@ -131,18 +128,26 @@ public final class FileSystemPackerMutationService implements PackerMutationServ
|
||||
List.of());
|
||||
}
|
||||
|
||||
private PackerMutationResult applyForget(
|
||||
private PackerMutationResult applyBuildParticipationChange(
|
||||
PackerProjectContext project,
|
||||
PackerRegistryState registry,
|
||||
Path assetRoot,
|
||||
PackerMutationPreview preview) {
|
||||
PackerMutationPreview preview,
|
||||
boolean includedInBuild) {
|
||||
final PackerRegistryState updated = registry.withAssets(
|
||||
registry.assets().stream()
|
||||
.filter(entry -> !PackerWorkspacePaths.assetRoot(project, entry.root()).equals(assetRoot))
|
||||
.map(entry -> PackerWorkspacePaths.assetRoot(project, entry.root()).equals(assetRoot)
|
||||
? new PackerRegistryEntry(entry.assetId(), entry.assetUuid(), entry.root(), includedInBuild)
|
||||
: entry)
|
||||
.toList(),
|
||||
registry.nextAssetId());
|
||||
workspaceFoundation.saveRegistry(project, updated);
|
||||
return new PackerMutationResult(PackerOperationStatus.SUCCESS, "Asset forgotten.", preview.operationId(), preview.proposedActions(), List.of());
|
||||
return new PackerMutationResult(
|
||||
PackerOperationStatus.SUCCESS,
|
||||
includedInBuild ? "Asset included in builds." : "Asset excluded from builds.",
|
||||
preview.operationId(),
|
||||
preview.proposedActions(),
|
||||
List.of());
|
||||
}
|
||||
|
||||
private PackerMutationResult applyRemove(
|
||||
@ -160,21 +165,6 @@ public final class FileSystemPackerMutationService implements PackerMutationServ
|
||||
return new PackerMutationResult(PackerOperationStatus.SUCCESS, "Asset removed from workspace.", preview.operationId(), preview.proposedActions(), List.of());
|
||||
}
|
||||
|
||||
private PackerMutationResult applyQuarantine(
|
||||
PackerProjectContext project,
|
||||
PackerRegistryState registry,
|
||||
Path assetRoot,
|
||||
PackerMutationPreview preview) {
|
||||
final PackerRegistryState updated = registry.withAssets(
|
||||
registry.assets().stream()
|
||||
.filter(entry -> !PackerWorkspacePaths.assetRoot(project, entry.root()).equals(assetRoot))
|
||||
.toList(),
|
||||
registry.nextAssetId());
|
||||
workspaceFoundation.saveRegistry(project, updated);
|
||||
moveAssetRoot(assetRoot, requireTarget(preview));
|
||||
return new PackerMutationResult(PackerOperationStatus.SUCCESS, "Asset moved to quarantine.", preview.operationId(), preview.proposedActions(), List.of());
|
||||
}
|
||||
|
||||
private PackerMutationResult applyRelocate(
|
||||
PackerProjectContext project,
|
||||
PackerRegistryState registry,
|
||||
@ -184,7 +174,11 @@ public final class FileSystemPackerMutationService implements PackerMutationServ
|
||||
final PackerRegistryState updated = registry.withAssets(
|
||||
registry.assets().stream()
|
||||
.map(entry -> PackerWorkspacePaths.assetRoot(project, entry.root()).equals(assetRoot)
|
||||
? new PackerRegistryEntry(entry.assetId(), entry.assetUuid(), PackerWorkspacePaths.relativeAssetRoot(project, targetRoot))
|
||||
? new PackerRegistryEntry(
|
||||
entry.assetId(),
|
||||
entry.assetUuid(),
|
||||
PackerWorkspacePaths.relativeAssetRoot(project, targetRoot),
|
||||
entry.includedInBuild())
|
||||
: entry)
|
||||
.toList(),
|
||||
registry.nextAssetId());
|
||||
@ -206,11 +200,11 @@ public final class FileSystemPackerMutationService implements PackerMutationServ
|
||||
Path targetAssetRoot = null;
|
||||
|
||||
switch (request.type()) {
|
||||
case REGISTER_ASSET, ADOPT_ASSET -> {
|
||||
case REGISTER_ASSET -> {
|
||||
if (managed) {
|
||||
blockers.add("Asset is already managed.");
|
||||
blockers.add("Asset is already registered.");
|
||||
}
|
||||
if (assetDetails.summary().state() == PackerAssetState.INVALID) {
|
||||
if (assetDetails.diagnostics().stream().anyMatch(PackerDiagnostic::blocking)) {
|
||||
blockers.add("Asset declaration must be valid before registration.");
|
||||
}
|
||||
if (!blockers.isEmpty()) {
|
||||
@ -221,12 +215,24 @@ public final class FileSystemPackerMutationService implements PackerMutationServ
|
||||
warnings.add("Asset currently reports diagnostics and will still be registered.");
|
||||
}
|
||||
}
|
||||
case FORGET_ASSET -> {
|
||||
case INCLUDE_ASSET_IN_BUILD -> {
|
||||
if (!managed) {
|
||||
blockers.add("Only managed assets can be forgotten.");
|
||||
blockers.add("Only registered assets can be included in builds.");
|
||||
} else if (assetDetails.summary().buildParticipation() == p.packer.api.assets.PackerBuildParticipation.INCLUDED) {
|
||||
blockers.add("Asset is already included in builds.");
|
||||
} else {
|
||||
actions.add(new PackerProposedAction(PackerOperationClass.REGISTRY_MUTATION, "REMOVE", relativeRoot));
|
||||
warnings.add("The asset will leave the managed build set.");
|
||||
actions.add(new PackerProposedAction(PackerOperationClass.REGISTRY_MUTATION, "UPDATE", relativeRoot));
|
||||
warnings.add("The asset will remain registered and return to the build set.");
|
||||
}
|
||||
}
|
||||
case EXCLUDE_ASSET_FROM_BUILD -> {
|
||||
if (!managed) {
|
||||
blockers.add("Only registered assets can be excluded from builds.");
|
||||
} else if (assetDetails.summary().buildParticipation() == p.packer.api.assets.PackerBuildParticipation.EXCLUDED) {
|
||||
blockers.add("Asset is already excluded from builds.");
|
||||
} else {
|
||||
actions.add(new PackerProposedAction(PackerOperationClass.REGISTRY_MUTATION, "UPDATE", relativeRoot));
|
||||
warnings.add("The asset will remain registered but leave the build set.");
|
||||
}
|
||||
}
|
||||
case REMOVE_ASSET -> {
|
||||
@ -236,24 +242,6 @@ public final class FileSystemPackerMutationService implements PackerMutationServ
|
||||
actions.add(new PackerProposedAction(PackerOperationClass.WORKSPACE_MUTATION, "DELETE", relativeRoot));
|
||||
warnings.add("Physical files inside the asset root will be deleted.");
|
||||
}
|
||||
case QUARANTINE_ASSET -> {
|
||||
if (isInsideQuarantine(context.project(), assetRoot)) {
|
||||
blockers.add("Asset is already inside quarantine.");
|
||||
} else {
|
||||
targetAssetRoot = request.targetRoot() != null
|
||||
? request.targetRoot()
|
||||
: nextAvailablePath(quarantineRoot(context.project()), sanitizeSegment(assetDetails.summary().identity().assetName()));
|
||||
if (managed) {
|
||||
actions.add(new PackerProposedAction(PackerOperationClass.REGISTRY_MUTATION, "REMOVE", relativeRoot));
|
||||
warnings.add("Quarantining a managed asset removes it from the active registry.");
|
||||
}
|
||||
actions.add(new PackerProposedAction(
|
||||
PackerOperationClass.WORKSPACE_MUTATION,
|
||||
"MOVE",
|
||||
relativeRoot + " -> " + PackerWorkspacePaths.relativeAssetRoot(context.project(), targetAssetRoot)));
|
||||
warnings.add("Quarantine is explicit and reversible, but the asset will leave its current workspace location.");
|
||||
}
|
||||
}
|
||||
case RELOCATE_ASSET -> {
|
||||
targetAssetRoot = request.targetRoot() != null
|
||||
? request.targetRoot()
|
||||
@ -291,26 +279,15 @@ public final class FileSystemPackerMutationService implements PackerMutationServ
|
||||
final List<String> initialBlockers = assetDetails.diagnostics().stream()
|
||||
.filter(PackerDiagnostic::blocking)
|
||||
.map(PackerDiagnostic::message)
|
||||
.filter(message -> request.type() != PackerMutationType.FORGET_ASSET
|
||||
.filter(message -> request.type() != PackerMutationType.INCLUDE_ASSET_IN_BUILD
|
||||
&& request.type() != PackerMutationType.EXCLUDE_ASSET_FROM_BUILD
|
||||
&& request.type() != PackerMutationType.REMOVE_ASSET
|
||||
&& request.type() != PackerMutationType.QUARANTINE_ASSET
|
||||
&& request.type() != PackerMutationType.RELOCATE_ASSET)
|
||||
.toList();
|
||||
return new ResolvedMutationContext(request.project(), request, assetDetails, initialBlockers);
|
||||
}
|
||||
|
||||
private Path quarantineRoot(PackerProjectContext project) {
|
||||
return PackerWorkspacePaths.registryDirectory(project).resolve(QUARANTINE_DIR).toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
private boolean isInsideQuarantine(PackerProjectContext project, Path assetRoot) {
|
||||
return assetRoot.toAbsolutePath().normalize().startsWith(quarantineRoot(project));
|
||||
}
|
||||
|
||||
private Path relocationTarget(PackerProjectContext project, Path assetRoot, String assetName) {
|
||||
if (isInsideQuarantine(project, assetRoot)) {
|
||||
return nextAvailablePath(PackerWorkspacePaths.assetsRoot(project).resolve(RECOVERED_DIR), sanitizeSegment(assetName));
|
||||
}
|
||||
final Path siblingParent = assetRoot.getParent() == null ? PackerWorkspacePaths.assetsRoot(project) : assetRoot.getParent();
|
||||
return nextAvailablePath(siblingParent, assetRoot.getFileName().toString() + "-relocated");
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import p.packer.api.PackerProjectContext;
|
||||
import p.packer.api.assets.PackerAssetIdentity;
|
||||
import p.packer.api.assets.PackerAssetState;
|
||||
import p.packer.api.assets.PackerAssetSummary;
|
||||
import p.packer.api.assets.PackerBuildParticipation;
|
||||
import p.packer.api.diagnostics.PackerDiagnostic;
|
||||
import p.packer.api.diagnostics.PackerDiagnosticCategory;
|
||||
import p.packer.api.diagnostics.PackerDiagnosticSeverity;
|
||||
@ -160,9 +161,12 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
: "unknown";
|
||||
final boolean preload = parsed.declaration() != null && parsed.declaration().preloadEnabled();
|
||||
final boolean hasDiagnostics = !parsed.diagnostics().isEmpty();
|
||||
final PackerAssetState state = parsed.valid()
|
||||
? (registryEntry == null ? PackerAssetState.ORPHAN : PackerAssetState.MANAGED)
|
||||
: PackerAssetState.INVALID;
|
||||
final PackerAssetState state = registryEntry == null
|
||||
? PackerAssetState.UNREGISTERED
|
||||
: PackerAssetState.REGISTERED;
|
||||
final PackerBuildParticipation buildParticipation = state == PackerAssetState.REGISTERED
|
||||
? (registryEntry.includedInBuild() ? PackerBuildParticipation.INCLUDED : PackerBuildParticipation.EXCLUDED)
|
||||
: PackerBuildParticipation.EXCLUDED;
|
||||
return new PackerAssetSummary(
|
||||
new PackerAssetIdentity(
|
||||
registryEntry == null ? null : registryEntry.assetId(),
|
||||
@ -170,6 +174,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
assetName,
|
||||
assetRoot),
|
||||
state,
|
||||
buildParticipation,
|
||||
assetFamily,
|
||||
preload,
|
||||
hasDiagnostics);
|
||||
|
||||
@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import p.packer.api.PackerOperationStatus;
|
||||
import p.packer.api.PackerProjectContext;
|
||||
import p.packer.api.assets.PackerBuildParticipation;
|
||||
import p.packer.api.assets.PackerAssetState;
|
||||
import p.packer.api.workspace.GetAssetDetailsRequest;
|
||||
import p.packer.testing.PackerFixtureLocator;
|
||||
@ -20,28 +21,30 @@ final class PackerAssetDetailsServiceTest {
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void returnsManagedDetailsForRegisteredAssetReferenceById() throws Exception {
|
||||
void returnsRegisteredDetailsForRegisteredAssetReferenceById() throws Exception {
|
||||
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("managed"));
|
||||
final PackerAssetDetailsService service = new PackerAssetDetailsService();
|
||||
|
||||
final var result = service.getAssetDetails(new GetAssetDetailsRequest(project(projectRoot), "1"));
|
||||
|
||||
assertEquals(PackerOperationStatus.SUCCESS, result.status());
|
||||
assertEquals(PackerAssetState.MANAGED, result.details().summary().state());
|
||||
assertEquals(PackerAssetState.REGISTERED, result.details().summary().state());
|
||||
assertEquals(PackerBuildParticipation.INCLUDED, result.details().summary().buildParticipation());
|
||||
assertEquals("ui_atlas", result.details().summary().identity().assetName());
|
||||
assertEquals("TILES/indexed_v1", result.details().outputFormat());
|
||||
assertTrue(result.diagnostics().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsOrphanDetailsForValidUnregisteredRootReference() throws Exception {
|
||||
void returnsUnregisteredDetailsForValidUnregisteredRootReference() throws Exception {
|
||||
final Path projectRoot = copyFixture("workspaces/orphan-valid", tempDir.resolve("orphan"));
|
||||
final PackerAssetDetailsService service = new PackerAssetDetailsService();
|
||||
|
||||
final var result = service.getAssetDetails(new GetAssetDetailsRequest(project(projectRoot), "orphans/ui_sounds"));
|
||||
|
||||
assertEquals(PackerOperationStatus.SUCCESS, result.status());
|
||||
assertEquals(PackerAssetState.ORPHAN, result.details().summary().state());
|
||||
assertEquals(PackerAssetState.UNREGISTERED, result.details().summary().state());
|
||||
assertEquals(PackerBuildParticipation.EXCLUDED, result.details().summary().buildParticipation());
|
||||
assertEquals("ui_sounds", result.details().summary().identity().assetName());
|
||||
}
|
||||
|
||||
@ -53,7 +56,8 @@ final class PackerAssetDetailsServiceTest {
|
||||
final var result = service.getAssetDetails(new GetAssetDetailsRequest(project(projectRoot), "bad"));
|
||||
|
||||
assertEquals(PackerOperationStatus.FAILED, result.status());
|
||||
assertEquals(PackerAssetState.INVALID, result.details().summary().state());
|
||||
assertEquals(PackerAssetState.UNREGISTERED, result.details().summary().state());
|
||||
assertEquals(PackerBuildParticipation.EXCLUDED, result.details().summary().buildParticipation());
|
||||
assertFalse(result.diagnostics().isEmpty());
|
||||
}
|
||||
|
||||
@ -64,7 +68,8 @@ final class PackerAssetDetailsServiceTest {
|
||||
final var result = service.getAssetDetails(new GetAssetDetailsRequest(project(tempDir.resolve("empty")), "missing/root"));
|
||||
|
||||
assertEquals(PackerOperationStatus.FAILED, result.status());
|
||||
assertEquals(PackerAssetState.INVALID, result.details().summary().state());
|
||||
assertEquals(PackerAssetState.UNREGISTERED, result.details().summary().state());
|
||||
assertEquals(PackerBuildParticipation.EXCLUDED, result.details().summary().buildParticipation());
|
||||
assertTrue(result.diagnostics().stream().anyMatch(diagnostic -> diagnostic.message().contains("could not be resolved")));
|
||||
}
|
||||
|
||||
|
||||
@ -40,7 +40,7 @@ final class FileSystemPackerDoctorServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void expandedWorkspaceReportsOrphanAssetsAndRegisterSafeFixes() throws Exception {
|
||||
void expandedWorkspaceReportsUnregisteredAssetsAndRegisterSafeFixes() throws Exception {
|
||||
final Path projectRoot = createExpandedWorkspace();
|
||||
final FileSystemPackerDoctorService service = new FileSystemPackerDoctorService();
|
||||
|
||||
@ -52,7 +52,7 @@ final class FileSystemPackerDoctorServiceTest {
|
||||
assertEquals(PackerOperationStatus.PARTIAL, result.status());
|
||||
assertTrue(result.diagnostics().stream().anyMatch(diagnostic ->
|
||||
diagnostic.category() == PackerDiagnosticCategory.HYGIENE
|
||||
&& diagnostic.message().contains("Orphan asset is valid but not registered")));
|
||||
&& diagnostic.message().contains("not registered, so it stays excluded from builds")));
|
||||
assertTrue(result.safeFixes().contains("register_asset orphans/ui_sounds"));
|
||||
}
|
||||
|
||||
|
||||
@ -25,37 +25,6 @@ final class FileSystemPackerMutationServiceTest {
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void previewAndApplyQuarantineForManagedAssetShowsStructuredImpact() throws Exception {
|
||||
final Path projectRoot = createManagedAssetProject();
|
||||
final List<PackerEvent> events = new CopyOnWriteArrayList<>();
|
||||
final FileSystemPackerMutationService service = service(events);
|
||||
final PackerProjectContext project = project(projectRoot);
|
||||
|
||||
final PackerMutationPreview preview = service.preview(new PackerMutationRequest(
|
||||
project,
|
||||
PackerMutationType.QUARANTINE_ASSET,
|
||||
"1",
|
||||
null));
|
||||
|
||||
assertTrue(preview.canApply());
|
||||
assertFalse(preview.highRisk());
|
||||
assertNotNull(preview.targetAssetRoot());
|
||||
assertEquals(1, preview.proposedActions().stream().filter(action -> action.operationClass() == p.packer.api.PackerOperationClass.REGISTRY_MUTATION).count());
|
||||
assertEquals(1, preview.proposedActions().stream().filter(action -> action.operationClass() == p.packer.api.PackerOperationClass.WORKSPACE_MUTATION).count());
|
||||
|
||||
service.apply(preview);
|
||||
|
||||
assertFalse(Files.exists(projectRoot.resolve("assets/ui/atlas")));
|
||||
assertTrue(Files.isDirectory(preview.targetAssetRoot()));
|
||||
final String registryJson = Files.readString(projectRoot.resolve("assets/.prometeu/index.json"));
|
||||
assertFalse(registryJson.contains("\"root\" : \"ui/atlas\""));
|
||||
assertEquals(List.of(PackerEventKind.PREVIEW_READY, PackerEventKind.ASSET_CHANGED, PackerEventKind.ACTION_APPLIED), events.stream().map(PackerEvent::kind).toList());
|
||||
assertEquals(preview.operationId(), events.getFirst().operationId());
|
||||
assertEquals(preview.operationId(), events.get(1).operationId());
|
||||
assertEquals(preview.operationId(), events.get(2).operationId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyRelocatePreservesIdentityAndUpdatesRegistryRoot() throws Exception {
|
||||
final Path projectRoot = createManagedAssetProject();
|
||||
@ -104,6 +73,27 @@ final class FileSystemPackerMutationServiceTest {
|
||||
assertTrue(preview.blockers().stream().anyMatch(message -> message.contains("valid before registration")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyExcludeFromBuildPreservesIdentityAndMarksRegistryEntryExcluded() throws Exception {
|
||||
final Path projectRoot = createManagedAssetProject();
|
||||
final FileSystemPackerMutationService service = service(new CopyOnWriteArrayList<>());
|
||||
final PackerProjectContext project = project(projectRoot);
|
||||
|
||||
final PackerMutationPreview preview = service.preview(new PackerMutationRequest(
|
||||
project,
|
||||
PackerMutationType.EXCLUDE_ASSET_FROM_BUILD,
|
||||
"1",
|
||||
null));
|
||||
|
||||
assertTrue(preview.canApply());
|
||||
service.apply(preview);
|
||||
|
||||
final String registryJson = Files.readString(projectRoot.resolve("assets/.prometeu/index.json"));
|
||||
assertTrue(registryJson.contains("\"asset_id\" : 1"));
|
||||
assertTrue(registryJson.contains("\"included_in_build\" : false"));
|
||||
assertTrue(Files.isDirectory(projectRoot.resolve("assets/ui/atlas")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void emitsFailureLifecycleWhenApplyFails() throws Exception {
|
||||
final Path projectRoot = createManagedAssetProject();
|
||||
@ -151,7 +141,8 @@ final class FileSystemPackerMutationServiceTest {
|
||||
{
|
||||
"asset_id": 1,
|
||||
"asset_uuid": "uuid-1",
|
||||
"root": "ui/atlas"
|
||||
"root": "ui/atlas",
|
||||
"included_in_build": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import p.packer.api.PackerOperationStatus;
|
||||
import p.packer.api.PackerProjectContext;
|
||||
import p.packer.api.assets.PackerBuildParticipation;
|
||||
import p.packer.api.assets.PackerAssetState;
|
||||
import p.packer.api.events.PackerEvent;
|
||||
import p.packer.api.events.PackerEventKind;
|
||||
@ -23,7 +24,7 @@ final class FileSystemPackerWorkspaceServiceTest {
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void listsManagedAndOrphanAssetsFromWorkspaceScan() throws Exception {
|
||||
void listsRegisteredAndUnregisteredAssetsFromWorkspaceScan() throws Exception {
|
||||
final Path projectRoot = copyFixture("workspaces/read-mixed", tempDir.resolve("mixed"));
|
||||
final FileSystemPackerWorkspaceService service = new FileSystemPackerWorkspaceService();
|
||||
|
||||
@ -31,8 +32,8 @@ final class FileSystemPackerWorkspaceServiceTest {
|
||||
|
||||
assertEquals(PackerOperationStatus.SUCCESS, result.status());
|
||||
assertEquals(2, result.assets().size());
|
||||
assertTrue(result.assets().stream().anyMatch(asset -> asset.state() == PackerAssetState.MANAGED));
|
||||
assertTrue(result.assets().stream().anyMatch(asset -> asset.state() == PackerAssetState.ORPHAN));
|
||||
assertTrue(result.assets().stream().anyMatch(asset -> asset.state() == PackerAssetState.REGISTERED));
|
||||
assertTrue(result.assets().stream().anyMatch(asset -> asset.state() == PackerAssetState.UNREGISTERED));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -54,7 +55,8 @@ final class FileSystemPackerWorkspaceServiceTest {
|
||||
final var result = service.listAssets(new ListAssetsRequest(project(projectRoot)));
|
||||
|
||||
assertEquals(1, result.assets().size());
|
||||
assertEquals(PackerAssetState.INVALID, result.assets().getFirst().state());
|
||||
assertEquals(PackerAssetState.UNREGISTERED, result.assets().getFirst().state());
|
||||
assertEquals(PackerBuildParticipation.EXCLUDED, result.assets().getFirst().buildParticipation());
|
||||
assertTrue(result.assets().getFirst().hasDiagnostics());
|
||||
}
|
||||
|
||||
|
||||
@ -36,7 +36,13 @@ public final class StudioWorkspaceRailControl<T> extends VBox implements StudioC
|
||||
button.setPrefSize(44, 44);
|
||||
button.setMinSize(44, 44);
|
||||
button.setMaxSize(44, 44);
|
||||
button.getStyleClass().add("studio-workspace-rail-button");
|
||||
button.getStyleClass().addAll(
|
||||
"studio-button",
|
||||
"studio-button-ghost",
|
||||
"studio-button-pill",
|
||||
"studio-button-icon",
|
||||
"studio-button-toggle",
|
||||
"studio-workspace-rail-button");
|
||||
button.setTooltip(new Tooltip());
|
||||
button.getTooltip().textProperty().bind(item.label());
|
||||
button.setOnAction(ignored -> Objects.requireNonNull(onSelect, "onSelect").accept(item.id()));
|
||||
|
||||
@ -79,10 +79,13 @@ public enum I18n {
|
||||
|
||||
WORKSPACE_ASSETS("workspace.assets"),
|
||||
ASSETS_NAVIGATOR_TITLE("assets.navigator.title"),
|
||||
ASSETS_NAVIGATOR_ACTION_ADD("assets.navigator.action.add"),
|
||||
ASSETS_NAVIGATOR_ACTION_DOCTOR("assets.navigator.action.doctor"),
|
||||
ASSETS_NAVIGATOR_ACTION_PACK("assets.navigator.action.pack"),
|
||||
ASSETS_DETAILS_TITLE("assets.details.title"),
|
||||
ASSETS_SEARCH_PROMPT("assets.search.prompt"),
|
||||
ASSETS_FILTER_MANAGED("assets.filter.managed"),
|
||||
ASSETS_FILTER_ORPHAN("assets.filter.orphan"),
|
||||
ASSETS_FILTER_REGISTERED("assets.filter.registered"),
|
||||
ASSETS_FILTER_UNREGISTERED("assets.filter.unregistered"),
|
||||
ASSETS_FILTER_DIAGNOSTICS("assets.filter.diagnostics"),
|
||||
ASSETS_FILTER_PRELOAD("assets.filter.preload"),
|
||||
ASSETS_STATE_LOADING("assets.state.loading"),
|
||||
@ -90,8 +93,8 @@ public enum I18n {
|
||||
ASSETS_STATE_NO_RESULTS("assets.state.noResults"),
|
||||
ASSETS_STATE_READY("assets.state.ready"),
|
||||
ASSETS_STATE_ERROR("assets.state.error"),
|
||||
ASSETS_BADGE_MANAGED("assets.badge.managed"),
|
||||
ASSETS_BADGE_ORPHAN("assets.badge.orphan"),
|
||||
ASSETS_BADGE_REGISTERED("assets.badge.registered"),
|
||||
ASSETS_BADGE_UNREGISTERED("assets.badge.unregistered"),
|
||||
ASSETS_BADGE_PRELOAD("assets.badge.preload"),
|
||||
ASSETS_BADGE_DIAGNOSTICS("assets.badge.diagnostics"),
|
||||
ASSETS_SECTION_SUMMARY("assets.section.summary"),
|
||||
@ -99,15 +102,10 @@ public enum I18n {
|
||||
ASSETS_SECTION_INPUTS_PREVIEW("assets.section.inputsPreview"),
|
||||
ASSETS_SECTION_DIAGNOSTICS("assets.section.diagnostics"),
|
||||
ASSETS_SECTION_ACTIONS("assets.section.actions"),
|
||||
ASSETS_ACTIONS_PRIMARY("assets.actions.primary"),
|
||||
ASSETS_ACTIONS_SENSITIVE("assets.actions.sensitive"),
|
||||
ASSETS_ACTION_DOCTOR("assets.action.doctor"),
|
||||
ASSETS_ACTION_BUILD("assets.action.build"),
|
||||
ASSETS_ACTION_ADOPT("assets.action.adopt"),
|
||||
ASSETS_ACTION_REGISTER("assets.action.register"),
|
||||
ASSETS_ACTION_QUARANTINE("assets.action.quarantine"),
|
||||
ASSETS_ACTION_INCLUDE_IN_BUILD("assets.action.includeInBuild"),
|
||||
ASSETS_ACTION_EXCLUDE_FROM_BUILD("assets.action.excludeFromBuild"),
|
||||
ASSETS_ACTION_RELOCATE("assets.action.relocate"),
|
||||
ASSETS_ACTION_FORGET("assets.action.forget"),
|
||||
ASSETS_ACTION_REMOVE("assets.action.remove"),
|
||||
ASSETS_MUTATION_PREVIEW_TITLE("assets.mutation.previewTitle"),
|
||||
ASSETS_MUTATION_SECTION_CHANGES("assets.mutation.section.changes"),
|
||||
@ -127,15 +125,20 @@ public enum I18n {
|
||||
ASSETS_MUTATION_APPLY("assets.mutation.apply"),
|
||||
ASSETS_MUTATION_CONFIRM_TITLE("assets.mutation.confirm.title"),
|
||||
ASSETS_MUTATION_CONFIRM_HEADER("assets.mutation.confirm.header"),
|
||||
ASSETS_MUTATION_CONFIRM_BODY("assets.mutation.confirm.body"),
|
||||
ASSETS_LABEL_NAME("assets.label.name"),
|
||||
ASSETS_LABEL_STATE("assets.label.state"),
|
||||
ASSETS_LABEL_REGISTRATION("assets.label.registration"),
|
||||
ASSETS_LABEL_BUILD_PARTICIPATION("assets.label.buildParticipation"),
|
||||
ASSETS_LABEL_ASSET_ID("assets.label.assetId"),
|
||||
ASSETS_LABEL_TYPE("assets.label.type"),
|
||||
ASSETS_LABEL_LOCATION("assets.label.location"),
|
||||
ASSETS_LABEL_TARGET_LOCATION("assets.label.targetLocation"),
|
||||
ASSETS_LABEL_FORMAT("assets.label.format"),
|
||||
ASSETS_LABEL_CODEC("assets.label.codec"),
|
||||
ASSETS_LABEL_PRELOAD("assets.label.preload"),
|
||||
ASSETS_VALUE_REGISTERED("assets.value.registered"),
|
||||
ASSETS_VALUE_UNREGISTERED("assets.value.unregistered"),
|
||||
ASSETS_VALUE_INCLUDED("assets.value.included"),
|
||||
ASSETS_VALUE_EXCLUDED("assets.value.excluded"),
|
||||
ASSETS_VALUE_YES("assets.value.yes"),
|
||||
ASSETS_VALUE_NO("assets.value.no"),
|
||||
ASSETS_PROGRESS_IDLE("assets.progress.idle"),
|
||||
@ -160,6 +163,49 @@ public enum I18n {
|
||||
ASSETS_DETAILS_EMPTY("assets.details.empty"),
|
||||
ASSETS_DETAILS_READY("assets.details.ready"),
|
||||
ASSETS_DETAILS_NO_SELECTION("assets.details.noSelection"),
|
||||
ASSETS_ADD_WIZARD_TITLE("assets.addWizard.title"),
|
||||
ASSETS_ADD_WIZARD_DESCRIPTION("assets.addWizard.description"),
|
||||
ASSETS_ADD_WIZARD_STEP_ROOT_TITLE("assets.addWizard.step.root.title"),
|
||||
ASSETS_ADD_WIZARD_STEP_ROOT_DESCRIPTION("assets.addWizard.step.root.description"),
|
||||
ASSETS_ADD_WIZARD_STEP_DETAILS_TITLE("assets.addWizard.step.details.title"),
|
||||
ASSETS_ADD_WIZARD_STEP_DETAILS_DESCRIPTION("assets.addWizard.step.details.description"),
|
||||
ASSETS_ADD_WIZARD_STEP_SUMMARY_TITLE("assets.addWizard.step.summary.title"),
|
||||
ASSETS_ADD_WIZARD_STEP_SUMMARY_DESCRIPTION("assets.addWizard.step.summary.description"),
|
||||
ASSETS_ADD_WIZARD_LABEL_NAME("assets.addWizard.label.name"),
|
||||
ASSETS_ADD_WIZARD_LABEL_ROOT("assets.addWizard.label.root"),
|
||||
ASSETS_ADD_WIZARD_LABEL_TYPE("assets.addWizard.label.type"),
|
||||
ASSETS_ADD_WIZARD_LABEL_FORMAT("assets.addWizard.label.format"),
|
||||
ASSETS_ADD_WIZARD_LABEL_CODEC("assets.addWizard.label.codec"),
|
||||
ASSETS_ADD_WIZARD_LABEL_PRELOAD("assets.addWizard.label.preload"),
|
||||
ASSETS_ADD_WIZARD_PROMPT_TYPE("assets.addWizard.prompt.type"),
|
||||
ASSETS_ADD_WIZARD_PROMPT_FORMAT("assets.addWizard.prompt.format"),
|
||||
ASSETS_ADD_WIZARD_PROMPT_CODEC("assets.addWizard.prompt.codec"),
|
||||
ASSETS_ADD_WIZARD_ASSETS_ROOT_HINT("assets.addWizard.assetsRootHint"),
|
||||
ASSETS_ADD_WIZARD_BROWSE_TITLE("assets.addWizard.browse.title"),
|
||||
ASSETS_ADD_WIZARD_NOTE("assets.addWizard.note"),
|
||||
ASSETS_ADD_WIZARD_BUTTON_CREATE("assets.addWizard.button.create"),
|
||||
ASSETS_ADD_WIZARD_ERROR_NAME("assets.addWizard.error.name"),
|
||||
ASSETS_ADD_WIZARD_ERROR_ROOT("assets.addWizard.error.root"),
|
||||
ASSETS_ADD_WIZARD_ERROR_TYPE("assets.addWizard.error.type"),
|
||||
ASSETS_ADD_WIZARD_ERROR_FORMAT("assets.addWizard.error.format"),
|
||||
ASSETS_ADD_WIZARD_ERROR_CODEC("assets.addWizard.error.codec"),
|
||||
ASSETS_ADD_WIZARD_ERROR_UNSUPPORTED_COMBINATION("assets.addWizard.error.unsupportedCombination"),
|
||||
ASSETS_RELOCATE_WIZARD_TITLE("assets.relocateWizard.title"),
|
||||
ASSETS_RELOCATE_WIZARD_DESCRIPTION("assets.relocateWizard.description"),
|
||||
ASSETS_RELOCATE_WIZARD_STEP_DESTINATION_TITLE("assets.relocateWizard.step.destination.title"),
|
||||
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_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"),
|
||||
ASSETS_RELOCATE_WIZARD_LABEL_TARGET_ROOT("assets.relocateWizard.label.targetRoot"),
|
||||
ASSETS_RELOCATE_WIZARD_PROMPT_DESTINATION_NAME("assets.relocateWizard.prompt.destinationName"),
|
||||
ASSETS_RELOCATE_WIZARD_ASSETS_ROOT_HINT("assets.relocateWizard.assetsRootHint"),
|
||||
ASSETS_RELOCATE_WIZARD_BROWSE_TITLE("assets.relocateWizard.browse.title"),
|
||||
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"),
|
||||
WORKSPACE_DEBUG("workspace.debug"),
|
||||
|
||||
|
||||
|
||||
@ -81,16 +81,20 @@ public final class NewProjectWizard {
|
||||
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());
|
||||
|
||||
createButton.textProperty().bind(Container.i18n().bind(I18n.WIZARD_CREATE));
|
||||
createButton.getStyleClass().addAll("studio-button", "studio-button-primary");
|
||||
createButton.setOnAction(ignored -> finishCreate());
|
||||
|
||||
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 -> stage.close());
|
||||
|
||||
final HBox actions = new HBox(12, backButton, nextButton, createButton, cancelButton);
|
||||
@ -155,6 +159,7 @@ public final class NewProjectWizard {
|
||||
|
||||
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 -> browseForLocation());
|
||||
|
||||
final HBox row = new HBox(12, locationField, browseButton);
|
||||
|
||||
@ -88,15 +88,18 @@ public final class ProjectLauncherView extends BorderPane {
|
||||
|
||||
final Button openButton = new Button();
|
||||
openButton.textProperty().bind(Container.i18n().bind(I18n.LAUNCHER_OPEN_PROJECT));
|
||||
openButton.getStyleClass().addAll("studio-button", "studio-button-primary");
|
||||
openButton.disableProperty().bind(projectList.getSelectionModel().selectedItemProperty().isNull());
|
||||
openButton.setOnAction(ignored -> openSelectedProject());
|
||||
|
||||
final Button addButton = new Button();
|
||||
addButton.textProperty().bind(Container.i18n().bind(I18n.LAUNCHER_ADD_PROJECT));
|
||||
addButton.getStyleClass().addAll("studio-button", "studio-button-secondary");
|
||||
addButton.setOnAction(ignored -> addExistingProject());
|
||||
|
||||
final Button forgetButton = new Button();
|
||||
forgetButton.textProperty().bind(Container.i18n().bind(I18n.LAUNCHER_FORGET_PROJECT));
|
||||
forgetButton.getStyleClass().addAll("studio-button", "studio-button-warning");
|
||||
forgetButton.disableProperty().bind(projectList.getSelectionModel().selectedItemProperty().isNull());
|
||||
forgetButton.setOnAction(ignored -> forgetSelectedProject());
|
||||
|
||||
@ -109,6 +112,7 @@ public final class ProjectLauncherView extends BorderPane {
|
||||
|
||||
final Button createButton = new Button();
|
||||
createButton.textProperty().bind(Container.i18n().bind(I18n.LAUNCHER_CREATE_BUTTON));
|
||||
createButton.getStyleClass().addAll("studio-button", "studio-button-primary");
|
||||
createButton.setOnAction(ignored -> openCreateWizard());
|
||||
|
||||
final HBox createRow = new HBox(10, createButton);
|
||||
|
||||
@ -0,0 +1,67 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class AssetCreationCatalog {
|
||||
private static final List<AssetTypeOption> TYPES = List.of(
|
||||
new AssetTypeOption(
|
||||
"image_bank",
|
||||
List.of(
|
||||
new AssetFormatOption("TILES/indexed_v1", List.of("RAW")),
|
||||
new AssetFormatOption("SPRITES/indexed_v1", List.of("RAW")))),
|
||||
new AssetTypeOption(
|
||||
"sound_bank",
|
||||
List.of(new AssetFormatOption("AUDIO/pcm_v1", List.of("RAW")))),
|
||||
new AssetTypeOption(
|
||||
"palette_bank",
|
||||
List.of(new AssetFormatOption("PALETTE/indexed_v1", List.of("RAW")))));
|
||||
|
||||
private AssetCreationCatalog() {
|
||||
}
|
||||
|
||||
public static List<String> assetTypes() {
|
||||
return TYPES.stream().map(AssetTypeOption::assetType).toList();
|
||||
}
|
||||
|
||||
public static List<String> outputFormatsFor(String assetType) {
|
||||
return findType(assetType).formats().stream().map(AssetFormatOption::outputFormat).toList();
|
||||
}
|
||||
|
||||
public static List<String> outputCodecsFor(String assetType, String outputFormat) {
|
||||
final AssetTypeOption type = findType(assetType);
|
||||
return type.formats().stream()
|
||||
.filter(format -> format.outputFormat().equals(outputFormat))
|
||||
.findFirst()
|
||||
.map(AssetFormatOption::codecs)
|
||||
.orElse(List.of());
|
||||
}
|
||||
|
||||
public static boolean supports(String assetType, String outputFormat, String outputCodec) {
|
||||
return outputCodecsFor(assetType, outputFormat).contains(outputCodec);
|
||||
}
|
||||
|
||||
private static AssetTypeOption findType(String assetType) {
|
||||
final String normalized = Objects.requireNonNullElse(assetType, "").trim();
|
||||
return TYPES.stream()
|
||||
.filter(type -> type.assetType().equals(normalized))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unsupported asset type: " + assetType));
|
||||
}
|
||||
|
||||
record AssetTypeOption(
|
||||
String assetType,
|
||||
List<AssetFormatOption> formats) {
|
||||
AssetTypeOption {
|
||||
formats = List.copyOf(formats);
|
||||
}
|
||||
}
|
||||
|
||||
record AssetFormatOption(
|
||||
String outputFormat,
|
||||
List<String> codecs) {
|
||||
AssetFormatOption {
|
||||
codecs = List.copyOf(codecs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record AssetCreationRequest(
|
||||
String assetName,
|
||||
String relativeRoot,
|
||||
String assetType,
|
||||
String outputFormat,
|
||||
String outputCodec,
|
||||
boolean preloadEnabled) {
|
||||
|
||||
public AssetCreationRequest {
|
||||
assetName = Objects.requireNonNull(assetName, "assetName").trim();
|
||||
relativeRoot = Objects.requireNonNull(relativeRoot, "relativeRoot").trim();
|
||||
assetType = Objects.requireNonNull(assetType, "assetType").trim();
|
||||
outputFormat = Objects.requireNonNull(outputFormat, "outputFormat").trim();
|
||||
outputCodec = Objects.requireNonNull(outputCodec, "outputCodec").trim();
|
||||
if (assetName.isBlank()
|
||||
|| relativeRoot.isBlank()
|
||||
|| assetType.isBlank()
|
||||
|| outputFormat.isBlank()
|
||||
|| outputCodec.isBlank()) {
|
||||
throw new IllegalArgumentException("Asset creation fields must not be blank.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
public record AssetCreationResult(
|
||||
AssetWorkspaceSelectionKey selectionKey,
|
||||
Path assetRoot) {
|
||||
|
||||
public AssetCreationResult {
|
||||
selectionKey = Objects.requireNonNull(selectionKey, "selectionKey");
|
||||
assetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
import p.studio.projects.ProjectReference;
|
||||
|
||||
public interface AssetCreationService {
|
||||
AssetCreationResult create(ProjectReference projectReference, AssetCreationRequest request);
|
||||
}
|
||||
@ -0,0 +1,354 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.Window;
|
||||
import p.studio.Container;
|
||||
import p.studio.projects.ProjectReference;
|
||||
import p.studio.utilities.i18n.I18n;
|
||||
|
||||
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 AssetCreationWizard {
|
||||
private final AssetCreationService assetCreationService;
|
||||
private final ProjectReference projectReference;
|
||||
private final Stage stage;
|
||||
private final AtomicReference<AssetCreationResult> 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 TextField rootField = new TextField();
|
||||
private final TextField nameField = new TextField();
|
||||
private final ComboBox<String> typeCombo = new ComboBox<>();
|
||||
private final ComboBox<String> formatCombo = new ComboBox<>();
|
||||
private final ComboBox<String> codecCombo = new ComboBox<>();
|
||||
private final CheckBox preloadField = new CheckBox();
|
||||
|
||||
private final Button backButton = new Button();
|
||||
private final Button nextButton = new Button();
|
||||
private final Button createButton = new Button();
|
||||
private final Button cancelButton = new Button();
|
||||
|
||||
private int stepIndex = 0;
|
||||
|
||||
private AssetCreationWizard(
|
||||
Window owner,
|
||||
ProjectReference projectReference,
|
||||
AssetCreationService assetCreationService) {
|
||||
this.projectReference = Objects.requireNonNull(projectReference, "projectReference");
|
||||
this.assetCreationService = Objects.requireNonNull(assetCreationService, "assetCreationService");
|
||||
this.stage = new Stage();
|
||||
stage.initOwner(owner);
|
||||
stage.initModality(Modality.WINDOW_MODAL);
|
||||
stage.setTitle(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_TITLE));
|
||||
stage.setScene(new Scene(buildRoot(), 620, 460));
|
||||
stage.getScene().getStylesheets().add(Container.theme().getDefaultTheme());
|
||||
|
||||
configureControls();
|
||||
renderStep();
|
||||
}
|
||||
|
||||
public static Optional<AssetCreationResult> showAndWait(
|
||||
Window owner,
|
||||
ProjectReference projectReference,
|
||||
AssetCreationService assetCreationService) {
|
||||
final AssetCreationWizard wizard = new AssetCreationWizard(owner, projectReference, assetCreationService);
|
||||
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");
|
||||
stepDescription.setWrapText(true);
|
||||
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());
|
||||
|
||||
createButton.textProperty().bind(Container.i18n().bind(I18n.ASSETS_ADD_WIZARD_BUTTON_CREATE));
|
||||
createButton.getStyleClass().addAll("studio-button", "studio-button-primary");
|
||||
createButton.setOnAction(ignored -> createAsset());
|
||||
|
||||
cancelButton.textProperty().bind(Container.i18n().bind(I18n.WIZARD_CANCEL));
|
||||
cancelButton.getStyleClass().addAll("studio-button", "studio-button-cancel");
|
||||
cancelButton.setOnAction(ignored -> stage.close());
|
||||
|
||||
final Region spacer = new Region();
|
||||
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||
final HBox actions = new HBox(12, backButton, spacer, cancelButton, nextButton, createButton);
|
||||
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 configureControls() {
|
||||
rootField.setPromptText("ui/new_asset");
|
||||
nameField.setPromptText(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_NAME));
|
||||
typeCombo.setPromptText(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_PROMPT_TYPE));
|
||||
formatCombo.setPromptText(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_PROMPT_FORMAT));
|
||||
codecCombo.setPromptText(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_PROMPT_CODEC));
|
||||
typeCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
formatCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
codecCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
typeCombo.setItems(FXCollections.observableArrayList(AssetCreationCatalog.assetTypes()));
|
||||
formatCombo.setDisable(true);
|
||||
codecCombo.setDisable(true);
|
||||
preloadField.setSelected(true);
|
||||
preloadField.textProperty().bind(Container.i18n().bind(I18n.ASSETS_ADD_WIZARD_LABEL_PRELOAD));
|
||||
|
||||
typeCombo.valueProperty().addListener((ignored, oldValue, newValue) -> {
|
||||
if (Objects.equals(oldValue, newValue)) {
|
||||
return;
|
||||
}
|
||||
formatCombo.setItems(FXCollections.observableArrayList());
|
||||
formatCombo.getSelectionModel().clearSelection();
|
||||
codecCombo.setItems(FXCollections.observableArrayList());
|
||||
codecCombo.getSelectionModel().clearSelection();
|
||||
codecCombo.setDisable(true);
|
||||
if (newValue == null || newValue.isBlank()) {
|
||||
formatCombo.setDisable(true);
|
||||
return;
|
||||
}
|
||||
formatCombo.setItems(FXCollections.observableArrayList(AssetCreationCatalog.outputFormatsFor(newValue)));
|
||||
formatCombo.setDisable(false);
|
||||
});
|
||||
|
||||
formatCombo.valueProperty().addListener((ignored, oldValue, newValue) -> {
|
||||
if (Objects.equals(oldValue, newValue)) {
|
||||
return;
|
||||
}
|
||||
codecCombo.setItems(FXCollections.observableArrayList());
|
||||
codecCombo.getSelectionModel().clearSelection();
|
||||
if (newValue == null || newValue.isBlank() || typeCombo.getValue() == null || typeCombo.getValue().isBlank()) {
|
||||
codecCombo.setDisable(true);
|
||||
return;
|
||||
}
|
||||
codecCombo.setItems(FXCollections.observableArrayList(AssetCreationCatalog.outputCodecsFor(typeCombo.getValue(), newValue)));
|
||||
codecCombo.setDisable(false);
|
||||
});
|
||||
}
|
||||
|
||||
private void renderStep() {
|
||||
feedbackLabel.setText("");
|
||||
backButton.setDisable(stepIndex == 0);
|
||||
nextButton.setVisible(stepIndex < 2);
|
||||
nextButton.setManaged(stepIndex < 2);
|
||||
createButton.setVisible(stepIndex == 2);
|
||||
createButton.setManaged(stepIndex == 2);
|
||||
|
||||
switch (stepIndex) {
|
||||
case 0 -> renderRootStep();
|
||||
case 1 -> renderMetadataStep();
|
||||
case 2 -> renderSummaryStep();
|
||||
default -> throw new IllegalStateException("Unknown asset creation step: " + stepIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private void renderRootStep() {
|
||||
stepTitle.setText(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_STEP_ROOT_TITLE));
|
||||
stepDescription.setText(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_STEP_ROOT_DESCRIPTION));
|
||||
|
||||
final Label assetsRootLabel = new Label(Container.i18n().format(
|
||||
I18n.ASSETS_ADD_WIZARD_ASSETS_ROOT_HINT,
|
||||
AssetRootValidator.assetsRoot(projectReference).toString()));
|
||||
assetsRootLabel.getStyleClass().add("studio-launcher-subtitle");
|
||||
assetsRootLabel.setWrapText(true);
|
||||
|
||||
final Button browseButton = new Button(Container.i18n().text(I18n.WIZARD_BROWSE));
|
||||
browseButton.getStyleClass().addAll("studio-button", "studio-button-secondary");
|
||||
browseButton.setOnAction(ignored -> browseForRoot());
|
||||
|
||||
final HBox row = new HBox(12, rootField, browseButton);
|
||||
row.setAlignment(Pos.CENTER_LEFT);
|
||||
HBox.setHgrow(rootField, Priority.ALWAYS);
|
||||
|
||||
stepBody.getChildren().setAll(
|
||||
field(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_ROOT), row),
|
||||
assetsRootLabel);
|
||||
}
|
||||
|
||||
private void renderMetadataStep() {
|
||||
stepTitle.setText(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_STEP_DETAILS_TITLE));
|
||||
stepDescription.setText(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_STEP_DETAILS_DESCRIPTION));
|
||||
|
||||
stepBody.getChildren().setAll(
|
||||
field(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_NAME), nameField),
|
||||
field(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_TYPE), typeCombo),
|
||||
field(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_FORMAT), formatCombo),
|
||||
field(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_CODEC), codecCombo));
|
||||
}
|
||||
|
||||
private void renderSummaryStep() {
|
||||
stepTitle.setText(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_STEP_SUMMARY_TITLE));
|
||||
stepDescription.setText(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_STEP_SUMMARY_DESCRIPTION));
|
||||
|
||||
final AssetRootValidationResult rootValidation = AssetRootValidator.validate(projectReference, rootField.getText().trim());
|
||||
final VBox summary = new VBox(8);
|
||||
summary.getChildren().addAll(
|
||||
summaryRow(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_ROOT), rootValidation.normalizedRelativeRoot()),
|
||||
summaryRow(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_NAME), nameField.getText().trim()),
|
||||
summaryRow(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_TYPE), Objects.requireNonNullElse(typeCombo.getValue(), "—")),
|
||||
summaryRow(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_FORMAT), Objects.requireNonNullElse(formatCombo.getValue(), "—")),
|
||||
summaryRow(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_CODEC), Objects.requireNonNullElse(codecCombo.getValue(), "—")));
|
||||
|
||||
final Label note = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_NOTE));
|
||||
note.getStyleClass().add("studio-launcher-subtitle");
|
||||
note.setWrapText(true);
|
||||
|
||||
stepBody.getChildren().setAll(summary, preloadField, note);
|
||||
}
|
||||
|
||||
private VBox field(String labelText, Node control) {
|
||||
final Label label = new Label(labelText);
|
||||
final VBox box = new VBox(6, label, control);
|
||||
VBox.setVgrow(control, Priority.NEVER);
|
||||
return box;
|
||||
}
|
||||
|
||||
private Node summaryRow(String key, String value) {
|
||||
final HBox row = new HBox(12);
|
||||
final Label keyLabel = new Label(key);
|
||||
keyLabel.getStyleClass().add("assets-details-key");
|
||||
final Label valueLabel = new Label(value == null || value.isBlank() ? "—" : value);
|
||||
valueLabel.getStyleClass().add("assets-details-value");
|
||||
valueLabel.setWrapText(true);
|
||||
HBox.setHgrow(valueLabel, Priority.ALWAYS);
|
||||
row.getChildren().addAll(keyLabel, valueLabel);
|
||||
return row;
|
||||
}
|
||||
|
||||
private void goBack() {
|
||||
if (stepIndex == 0) {
|
||||
return;
|
||||
}
|
||||
stepIndex -= 1;
|
||||
renderStep();
|
||||
}
|
||||
|
||||
private void goNext() {
|
||||
if (!validateCurrentStep()) {
|
||||
return;
|
||||
}
|
||||
stepIndex += 1;
|
||||
renderStep();
|
||||
}
|
||||
|
||||
private boolean validateCurrentStep() {
|
||||
return switch (stepIndex) {
|
||||
case 0 -> validateRootStep();
|
||||
case 1 -> validateMetadataStep();
|
||||
default -> true;
|
||||
};
|
||||
}
|
||||
|
||||
private boolean validateRootStep() {
|
||||
final AssetRootValidationResult validation = AssetRootValidator.validate(projectReference, rootField.getText().trim());
|
||||
if (!validation.valid()) {
|
||||
feedbackLabel.setText(validation.message().isBlank()
|
||||
? Container.i18n().text(I18n.ASSETS_ADD_WIZARD_ERROR_ROOT)
|
||||
: validation.message());
|
||||
return false;
|
||||
}
|
||||
rootField.setText(validation.normalizedRelativeRoot());
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean validateMetadataStep() {
|
||||
if (nameField.getText().trim().isBlank()) {
|
||||
feedbackLabel.setText(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_ERROR_NAME));
|
||||
return false;
|
||||
}
|
||||
if (typeCombo.getValue() == null || typeCombo.getValue().isBlank()) {
|
||||
feedbackLabel.setText(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_ERROR_TYPE));
|
||||
return false;
|
||||
}
|
||||
if (formatCombo.getValue() == null || formatCombo.getValue().isBlank()) {
|
||||
feedbackLabel.setText(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_ERROR_FORMAT));
|
||||
return false;
|
||||
}
|
||||
if (codecCombo.getValue() == null || codecCombo.getValue().isBlank()) {
|
||||
feedbackLabel.setText(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_ERROR_CODEC));
|
||||
return false;
|
||||
}
|
||||
if (!AssetCreationCatalog.supports(typeCombo.getValue(), formatCombo.getValue(), codecCombo.getValue())) {
|
||||
feedbackLabel.setText(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_ERROR_UNSUPPORTED_COMBINATION));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void createAsset() {
|
||||
feedbackLabel.setText("");
|
||||
if (!validateRootStep() || !validateMetadataStep()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
result.set(assetCreationService.create(projectReference, new AssetCreationRequest(
|
||||
nameField.getText().trim(),
|
||||
rootField.getText().trim(),
|
||||
typeCombo.getValue(),
|
||||
formatCombo.getValue(),
|
||||
codecCombo.getValue(),
|
||||
preloadField.isSelected())));
|
||||
stage.close();
|
||||
} catch (RuntimeException runtimeException) {
|
||||
feedbackLabel.setText(Objects.requireNonNullElse(runtimeException.getMessage(), "Unable to create asset."));
|
||||
}
|
||||
}
|
||||
|
||||
private void browseForRoot() {
|
||||
final DirectoryChooser chooser = new DirectoryChooser();
|
||||
chooser.setTitle(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_BROWSE_TITLE));
|
||||
final File initialDirectory = AssetRootValidator.assetsRoot(projectReference).toFile();
|
||||
if (initialDirectory.isDirectory()) {
|
||||
chooser.setInitialDirectory(initialDirectory);
|
||||
} else if (projectReference.rootPath().toFile().isDirectory()) {
|
||||
chooser.setInitialDirectory(projectReference.rootPath().toFile());
|
||||
}
|
||||
final File selected = chooser.showDialog(stage);
|
||||
if (selected == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final AssetRootValidationResult validation = AssetRootValidator.fromAbsoluteDirectory(projectReference, selected.toPath());
|
||||
if (!validation.valid()) {
|
||||
feedbackLabel.setText(validation.message());
|
||||
return;
|
||||
}
|
||||
rootField.setText(validation.normalizedRelativeRoot());
|
||||
feedbackLabel.setText("");
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
public enum AssetNavigatorFilter {
|
||||
MANAGED,
|
||||
ORPHAN,
|
||||
REGISTERED,
|
||||
UNREGISTERED,
|
||||
DIAGNOSTICS,
|
||||
PRELOAD
|
||||
}
|
||||
|
||||
@ -44,11 +44,11 @@ public final class AssetNavigatorProjectionBuilder {
|
||||
return true;
|
||||
}
|
||||
|
||||
final boolean includeManaged = filters.contains(AssetNavigatorFilter.MANAGED);
|
||||
final boolean includeOrphan = filters.contains(AssetNavigatorFilter.ORPHAN);
|
||||
if (includeManaged || includeOrphan) {
|
||||
final boolean stateMatches = (includeManaged && asset.state() == AssetWorkspaceAssetState.MANAGED)
|
||||
|| (includeOrphan && asset.state() == AssetWorkspaceAssetState.ORPHAN);
|
||||
final boolean includeRegistered = filters.contains(AssetNavigatorFilter.REGISTERED);
|
||||
final boolean includeUnregistered = filters.contains(AssetNavigatorFilter.UNREGISTERED);
|
||||
if (includeRegistered || includeUnregistered) {
|
||||
final boolean stateMatches = (includeRegistered && asset.state() == AssetWorkspaceAssetState.REGISTERED)
|
||||
|| (includeUnregistered && asset.state() == AssetWorkspaceAssetState.UNREGISTERED);
|
||||
if (!stateMatches) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -0,0 +1,113 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
import p.studio.projects.ProjectReference;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
final class AssetRelocationTargetValidator {
|
||||
private static final String CONTROL_DIR = ".prometeu";
|
||||
|
||||
private AssetRelocationTargetValidator() {
|
||||
}
|
||||
|
||||
static AssetRootValidationResult validate(
|
||||
ProjectReference projectReference,
|
||||
Path currentAssetRoot,
|
||||
String parentRelativeRootText,
|
||||
String destinationName) {
|
||||
final String normalizedName = normalizeDestinationName(destinationName);
|
||||
if (normalizedName == null) {
|
||||
return AssetRootValidationResult.failure("Destination folder name is required.");
|
||||
}
|
||||
|
||||
final Path parentRelativeRoot;
|
||||
try {
|
||||
parentRelativeRoot = normalizeParentRelativeRoot(parentRelativeRootText);
|
||||
} catch (RuntimeException runtimeException) {
|
||||
return AssetRootValidationResult.failure("Destination parent must stay inside assets/.");
|
||||
}
|
||||
|
||||
final String relativeRootText = parentRelativeRoot.getNameCount() == 0
|
||||
? normalizedName
|
||||
: parentRelativeRoot.resolve(normalizedName).toString().replace('\\', '/');
|
||||
return validate(projectReference, currentAssetRoot, AssetRootValidator.assetsRoot(projectReference).resolve(relativeRootText));
|
||||
}
|
||||
|
||||
static AssetRootValidationResult validate(
|
||||
ProjectReference projectReference,
|
||||
Path currentAssetRoot,
|
||||
Path targetRoot) {
|
||||
Objects.requireNonNull(projectReference, "projectReference");
|
||||
Objects.requireNonNull(currentAssetRoot, "currentAssetRoot");
|
||||
if (targetRoot == null) {
|
||||
return AssetRootValidationResult.failure("Relocation target is required.");
|
||||
}
|
||||
|
||||
final Path assetsRoot = AssetRootValidator.assetsRoot(projectReference);
|
||||
final Path normalizedTargetRoot = targetRoot.toAbsolutePath().normalize();
|
||||
if (!normalizedTargetRoot.startsWith(assetsRoot)) {
|
||||
return AssetRootValidationResult.failure("Relocation target must stay inside assets/.");
|
||||
}
|
||||
|
||||
final Path relativeRoot = assetsRoot.relativize(normalizedTargetRoot);
|
||||
if (relativeRoot.getNameCount() == 0) {
|
||||
return AssetRootValidationResult.failure("Relocation target must point to an asset root inside assets/.");
|
||||
}
|
||||
if (CONTROL_DIR.equals(relativeRoot.getName(0).toString())) {
|
||||
return AssetRootValidationResult.failure("Relocation target must not use the reserved .prometeu directory.");
|
||||
}
|
||||
|
||||
final Path normalizedCurrentRoot = currentAssetRoot.toAbsolutePath().normalize();
|
||||
if (normalizedTargetRoot.equals(normalizedCurrentRoot)) {
|
||||
return AssetRootValidationResult.failure("Relocation target must be different from the current asset root.");
|
||||
}
|
||||
if (normalizedTargetRoot.startsWith(normalizedCurrentRoot)) {
|
||||
return AssetRootValidationResult.failure("Relocation target must not be inside the current asset root.");
|
||||
}
|
||||
if (Files.exists(normalizedTargetRoot)) {
|
||||
return AssetRootValidationResult.failure("Relocation target already exists: "
|
||||
+ relativeRoot.toString().replace('\\', '/'));
|
||||
}
|
||||
|
||||
return AssetRootValidationResult.success(relativeRoot.toString().replace('\\', '/'), normalizedTargetRoot);
|
||||
}
|
||||
|
||||
static String normalizeParentForDisplay(Path assetsRoot, Path parentDirectory) {
|
||||
final Path normalizedAssetsRoot = Objects.requireNonNull(assetsRoot, "assetsRoot").toAbsolutePath().normalize();
|
||||
final Path normalizedParent = Objects.requireNonNull(parentDirectory, "parentDirectory").toAbsolutePath().normalize();
|
||||
if (!normalizedParent.startsWith(normalizedAssetsRoot)) {
|
||||
throw new IllegalArgumentException("Parent directory must stay inside assets/");
|
||||
}
|
||||
final Path relativeParent = normalizedAssetsRoot.relativize(normalizedParent);
|
||||
return relativeParent.getNameCount() == 0 ? "." : relativeParent.toString().replace('\\', '/');
|
||||
}
|
||||
|
||||
private static Path normalizeParentRelativeRoot(String parentRelativeRootText) {
|
||||
final String trimmed = Objects.requireNonNullElse(parentRelativeRootText, "").trim();
|
||||
if (trimmed.isBlank() || ".".equals(trimmed)) {
|
||||
return Path.of("");
|
||||
}
|
||||
final Path relativeRoot = Path.of(trimmed).normalize();
|
||||
if (relativeRoot.isAbsolute() || relativeRoot.startsWith("..")) {
|
||||
throw new IllegalArgumentException("Parent directory must stay inside assets/");
|
||||
}
|
||||
if (relativeRoot.getNameCount() > 0 && CONTROL_DIR.equals(relativeRoot.getName(0).toString())) {
|
||||
throw new IllegalArgumentException("Reserved directory");
|
||||
}
|
||||
return relativeRoot;
|
||||
}
|
||||
|
||||
private static String normalizeDestinationName(String destinationName) {
|
||||
final String trimmed = Objects.requireNonNullElse(destinationName, "").trim();
|
||||
if (trimmed.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
final Path segment = Path.of(trimmed).normalize();
|
||||
if (segment.isAbsolute() || segment.getNameCount() != 1 || ".".equals(trimmed) || "..".equals(trimmed)) {
|
||||
return null;
|
||||
}
|
||||
return segment.getFileName().toString();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,417 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.Window;
|
||||
import p.studio.Container;
|
||||
import p.studio.projects.ProjectReference;
|
||||
import p.studio.utilities.i18n.I18n;
|
||||
|
||||
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 AssetRelocationWizard {
|
||||
private final ProjectReference projectReference;
|
||||
private final AssetWorkspaceAssetSummary asset;
|
||||
private final AssetWorkspaceMutationService mutationService;
|
||||
private final Stage stage;
|
||||
private final AtomicReference<AssetWorkspaceMutationPreview> result = new AtomicReference<>();
|
||||
|
||||
private final Label stepTitle = new Label();
|
||||
private final Label stepDescription = new Label();
|
||||
private final VBox stepBody = new VBox(12);
|
||||
private final TextField parentField = new TextField();
|
||||
private final TextField nameField = new TextField();
|
||||
private final Label targetRootValue = new Label("—");
|
||||
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 Button cancelButton = new Button();
|
||||
|
||||
private AssetWorkspaceMutationPreview preview;
|
||||
private int stepIndex = 0;
|
||||
|
||||
private AssetRelocationWizard(
|
||||
Window owner,
|
||||
ProjectReference projectReference,
|
||||
AssetWorkspaceAssetSummary asset,
|
||||
AssetWorkspaceMutationService mutationService) {
|
||||
this.projectReference = Objects.requireNonNull(projectReference, "projectReference");
|
||||
this.asset = Objects.requireNonNull(asset, "asset");
|
||||
this.mutationService = Objects.requireNonNull(mutationService, "mutationService");
|
||||
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(), 640, 440));
|
||||
stage.getScene().getStylesheets().add(Container.theme().getDefaultTheme());
|
||||
|
||||
configureDefaults();
|
||||
configureValidation();
|
||||
renderStep();
|
||||
}
|
||||
|
||||
public static Optional<AssetWorkspaceMutationPreview> showAndWait(
|
||||
Window owner,
|
||||
ProjectReference projectReference,
|
||||
AssetWorkspaceAssetSummary asset,
|
||||
AssetWorkspaceMutationService mutationService) {
|
||||
final AssetRelocationWizard wizard = new AssetRelocationWizard(owner, projectReference, asset, mutationService);
|
||||
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");
|
||||
stepDescription.setWrapText(true);
|
||||
|
||||
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 -> confirm());
|
||||
|
||||
cancelButton.textProperty().bind(Container.i18n().bind(I18n.WIZARD_CANCEL));
|
||||
cancelButton.getStyleClass().addAll("studio-button", "studio-button-cancel");
|
||||
cancelButton.setOnAction(ignored -> stage.close());
|
||||
|
||||
final Region spacer = new Region();
|
||||
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||
final HBox actions = new HBox(12, backButton, spacer, cancelButton, nextButton, confirmButton);
|
||||
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 Node field(String labelText, Node control) {
|
||||
final Label label = new Label(labelText);
|
||||
final VBox box = new VBox(6, label, control);
|
||||
VBox.setVgrow(control, Priority.NEVER);
|
||||
return box;
|
||||
}
|
||||
|
||||
private void configureDefaults() {
|
||||
final Path assetsRoot = AssetRootValidator.assetsRoot(projectReference);
|
||||
final Path currentRoot = asset.assetRoot().toAbsolutePath().normalize();
|
||||
final Path currentParent = currentRoot.getParent() == null ? assetsRoot : currentRoot.getParent();
|
||||
parentField.setText(AssetRelocationTargetValidator.normalizeParentForDisplay(assetsRoot, currentParent));
|
||||
nameField.setPromptText(Container.i18n().text(I18n.ASSETS_RELOCATE_WIZARD_PROMPT_DESTINATION_NAME));
|
||||
final String currentLeaf = currentRoot.getFileName() == null ? "" : currentRoot.getFileName().toString();
|
||||
nameField.setText(currentLeaf);
|
||||
}
|
||||
|
||||
private void configureValidation() {
|
||||
parentField.textProperty().addListener((ignored, oldValue, newValue) -> refreshValidation());
|
||||
nameField.textProperty().addListener((ignored, oldValue, newValue) -> refreshValidation());
|
||||
}
|
||||
|
||||
private void renderStep() {
|
||||
feedbackLabel.setText("");
|
||||
backButton.setDisable(stepIndex == 0);
|
||||
nextButton.setVisible(stepIndex == 0);
|
||||
nextButton.setManaged(stepIndex == 0);
|
||||
confirmButton.setVisible(stepIndex == 1);
|
||||
confirmButton.setManaged(stepIndex == 1);
|
||||
|
||||
switch (stepIndex) {
|
||||
case 0 -> renderDestinationStep();
|
||||
case 1 -> renderSummaryStep();
|
||||
default -> throw new IllegalStateException("Unknown relocation step: " + stepIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private void renderDestinationStep() {
|
||||
stepTitle.setText(Container.i18n().text(I18n.ASSETS_RELOCATE_WIZARD_STEP_DESTINATION_TITLE));
|
||||
stepDescription.setText(Container.i18n().text(I18n.ASSETS_RELOCATE_WIZARD_STEP_DESTINATION_DESCRIPTION));
|
||||
|
||||
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 -> browseForParent());
|
||||
|
||||
final HBox parentRow = new HBox(12, parentField, browseButton);
|
||||
parentRow.setAlignment(Pos.CENTER_LEFT);
|
||||
HBox.setHgrow(parentField, Priority.ALWAYS);
|
||||
|
||||
final Label currentRootValue = new Label(relativeAssetPath(asset.assetRoot()));
|
||||
currentRootValue.getStyleClass().add("assets-details-value");
|
||||
currentRootValue.setWrapText(true);
|
||||
|
||||
targetRootValue.getStyleClass().add("assets-details-value");
|
||||
targetRootValue.setWrapText(true);
|
||||
|
||||
stepBody.getChildren().setAll(
|
||||
field(Container.i18n().text(I18n.ASSETS_RELOCATE_WIZARD_LABEL_CURRENT_ROOT), currentRootValue),
|
||||
field(Container.i18n().text(I18n.ASSETS_RELOCATE_WIZARD_LABEL_DESTINATION_PARENT), parentRow),
|
||||
field(Container.i18n().text(I18n.ASSETS_RELOCATE_WIZARD_LABEL_DESTINATION_NAME), nameField),
|
||||
field(Container.i18n().text(I18n.ASSETS_RELOCATE_WIZARD_LABEL_TARGET_ROOT), targetRootValue));
|
||||
refreshValidation();
|
||||
}
|
||||
|
||||
private void renderSummaryStep() {
|
||||
stepTitle.setText(Container.i18n().text(I18n.ASSETS_RELOCATE_WIZARD_STEP_SUMMARY_TITLE));
|
||||
stepDescription.setText(Container.i18n().text(I18n.ASSETS_RELOCATE_WIZARD_STEP_SUMMARY_DESCRIPTION));
|
||||
|
||||
final AssetWorkspaceMutationPreview currentPreview = Objects.requireNonNull(preview, "preview");
|
||||
final Label note = new Label(Container.i18n().text(I18n.ASSETS_RELOCATE_WIZARD_NOTE));
|
||||
note.getStyleClass().add("studio-launcher-subtitle");
|
||||
note.setWrapText(true);
|
||||
|
||||
final VBox summary = new VBox(10);
|
||||
summary.getChildren().addAll(
|
||||
createMutationSection(
|
||||
Container.i18n().text(I18n.ASSETS_MUTATION_SECTION_AFFECTED_ASSET),
|
||||
createAffectedAssetContent(currentPreview)),
|
||||
createMutationSection(
|
||||
Container.i18n().text(I18n.ASSETS_MUTATION_SECTION_REGISTRY_IMPACT),
|
||||
createMutationChangesContent(
|
||||
AssetWorkspaceMutationImpactViewModel.from(currentPreview).registryChanges(),
|
||||
Container.i18n().text(I18n.ASSETS_MUTATION_EMPTY_REGISTRY_IMPACT))),
|
||||
createMutationSection(
|
||||
Container.i18n().text(I18n.ASSETS_MUTATION_SECTION_WORKSPACE_IMPACT),
|
||||
createMutationChangesContent(
|
||||
AssetWorkspaceMutationImpactViewModel.from(currentPreview).workspaceChanges(),
|
||||
Container.i18n().text(I18n.ASSETS_MUTATION_EMPTY_WORKSPACE_IMPACT))),
|
||||
createMutationSection(
|
||||
Container.i18n().text(I18n.ASSETS_MUTATION_SECTION_BLOCKERS),
|
||||
createMutationMessages(
|
||||
currentPreview.blockers(),
|
||||
"assets-mutation-message-blocker",
|
||||
Container.i18n().text(I18n.ASSETS_MUTATION_EMPTY_BLOCKERS))),
|
||||
createMutationSection(
|
||||
Container.i18n().text(I18n.ASSETS_MUTATION_SECTION_WARNINGS),
|
||||
createMutationMessages(
|
||||
currentPreview.warnings(),
|
||||
"assets-mutation-message-warning",
|
||||
Container.i18n().text(I18n.ASSETS_MUTATION_EMPTY_WARNINGS))),
|
||||
createMutationSection(
|
||||
Container.i18n().text(I18n.ASSETS_MUTATION_SECTION_SAFE_FIXES),
|
||||
createMutationMessages(
|
||||
currentPreview.safeFixes(),
|
||||
"assets-mutation-message-safe-fix",
|
||||
Container.i18n().text(I18n.ASSETS_MUTATION_EMPTY_SAFE_FIXES))),
|
||||
note);
|
||||
stepBody.getChildren().setAll(summary);
|
||||
confirmButton.setDisable(!currentPreview.canApply());
|
||||
}
|
||||
|
||||
private void refreshValidation() {
|
||||
final AssetRootValidationResult validation = currentValidation();
|
||||
if (!validation.valid()) {
|
||||
targetRootValue.setText("—");
|
||||
feedbackLabel.setText(validation.message());
|
||||
nextButton.setDisable(true);
|
||||
confirmButton.setDisable(true);
|
||||
return;
|
||||
}
|
||||
targetRootValue.setText(validation.normalizedRelativeRoot());
|
||||
feedbackLabel.setText("");
|
||||
nextButton.setDisable(false);
|
||||
confirmButton.setDisable(false);
|
||||
}
|
||||
|
||||
private void browseForParent() {
|
||||
final DirectoryChooser chooser = new DirectoryChooser();
|
||||
chooser.setTitle(Container.i18n().text(I18n.ASSETS_RELOCATE_WIZARD_BROWSE_TITLE));
|
||||
final File initialDirectory = initialParentDirectory().toFile();
|
||||
if (initialDirectory.isDirectory()) {
|
||||
chooser.setInitialDirectory(initialDirectory);
|
||||
}
|
||||
final File selected = chooser.showDialog(stage);
|
||||
if (selected == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Path selectedDirectory = selected.toPath().toAbsolutePath().normalize();
|
||||
final Path assetsRoot = AssetRootValidator.assetsRoot(projectReference);
|
||||
if (!selectedDirectory.startsWith(assetsRoot)) {
|
||||
feedbackLabel.setText("Destination parent must stay inside assets/.");
|
||||
return;
|
||||
}
|
||||
final Path relativeParent = assetsRoot.relativize(selectedDirectory);
|
||||
if (relativeParent.getNameCount() > 0 && ".prometeu".equals(relativeParent.getName(0).toString())) {
|
||||
feedbackLabel.setText("Destination parent must not use the reserved .prometeu directory.");
|
||||
return;
|
||||
}
|
||||
|
||||
parentField.setText(AssetRelocationTargetValidator.normalizeParentForDisplay(assetsRoot, selectedDirectory));
|
||||
refreshValidation();
|
||||
}
|
||||
|
||||
private Path initialParentDirectory() {
|
||||
final Path assetsRoot = AssetRootValidator.assetsRoot(projectReference);
|
||||
try {
|
||||
final String parentText = parentField.getText();
|
||||
if (parentText == null || parentText.isBlank() || ".".equals(parentText.trim())) {
|
||||
return assetsRoot;
|
||||
}
|
||||
final Path candidate = assetsRoot.resolve(parentField.getText().trim()).toAbsolutePath().normalize();
|
||||
return candidate.startsWith(assetsRoot) && candidate.toFile().isDirectory() ? candidate : assetsRoot;
|
||||
} catch (RuntimeException runtimeException) {
|
||||
return assetsRoot;
|
||||
}
|
||||
}
|
||||
|
||||
private void goBack() {
|
||||
if (stepIndex == 0) {
|
||||
return;
|
||||
}
|
||||
preview = null;
|
||||
stepIndex -= 1;
|
||||
renderStep();
|
||||
}
|
||||
|
||||
private void goNext() {
|
||||
final AssetRootValidationResult validation = currentValidation();
|
||||
if (!validation.valid()) {
|
||||
feedbackLabel.setText(validation.message());
|
||||
nextButton.setDisable(true);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
preview = mutationService.preview(projectReference, asset, AssetWorkspaceAction.RELOCATE, validation.assetRoot());
|
||||
} catch (RuntimeException runtimeException) {
|
||||
preview = null;
|
||||
feedbackLabel.setText(Objects.requireNonNullElse(runtimeException.getMessage(), "Unable to preview relocation."));
|
||||
return;
|
||||
}
|
||||
stepIndex = 1;
|
||||
renderStep();
|
||||
}
|
||||
|
||||
private void confirm() {
|
||||
final AssetWorkspaceMutationPreview currentPreview = preview;
|
||||
if (currentPreview == null) {
|
||||
feedbackLabel.setText("Relocation preview is required before confirmation.");
|
||||
confirmButton.setDisable(true);
|
||||
return;
|
||||
}
|
||||
if (!currentPreview.canApply()) {
|
||||
feedbackLabel.setText(currentPreview.blockers().isEmpty()
|
||||
? "Relocation cannot be applied."
|
||||
: currentPreview.blockers().getFirst());
|
||||
confirmButton.setDisable(true);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
mutationService.apply(projectReference, currentPreview);
|
||||
result.set(currentPreview);
|
||||
stage.close();
|
||||
} catch (RuntimeException runtimeException) {
|
||||
feedbackLabel.setText(Objects.requireNonNullElse(runtimeException.getMessage(), "Unable to apply relocation."));
|
||||
}
|
||||
}
|
||||
|
||||
private AssetRootValidationResult currentValidation() {
|
||||
return AssetRelocationTargetValidator.validate(
|
||||
projectReference,
|
||||
asset.assetRoot(),
|
||||
parentField.getText(),
|
||||
nameField.getText());
|
||||
}
|
||||
|
||||
private Node createMutationSection(String title, Node content) {
|
||||
final VBox section = new VBox(6);
|
||||
final Label label = new Label(title);
|
||||
label.getStyleClass().add("assets-mutation-section-title");
|
||||
section.getChildren().addAll(label, content);
|
||||
return section;
|
||||
}
|
||||
|
||||
private Node createAffectedAssetContent(AssetWorkspaceMutationPreview currentPreview) {
|
||||
final VBox box = new VBox(6);
|
||||
box.getChildren().add(keyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_NAME), currentPreview.asset().assetName()));
|
||||
box.getChildren().add(keyValueRow(
|
||||
Container.i18n().text(I18n.ASSETS_LABEL_LOCATION),
|
||||
relativeAssetPath(currentPreview.asset().assetRoot())));
|
||||
if (currentPreview.targetAssetRoot() != null) {
|
||||
box.getChildren().add(keyValueRow(
|
||||
Container.i18n().text(I18n.ASSETS_LABEL_TARGET_LOCATION),
|
||||
relativeAssetPath(currentPreview.targetAssetRoot())));
|
||||
}
|
||||
return box;
|
||||
}
|
||||
|
||||
private Node createMutationChangesContent(java.util.List<AssetWorkspaceMutationChange> changes, String emptyText) {
|
||||
if (changes.isEmpty()) {
|
||||
final Label label = new Label(emptyText);
|
||||
label.setWrapText(true);
|
||||
label.getStyleClass().add("assets-details-section-message");
|
||||
return label;
|
||||
}
|
||||
final VBox box = new VBox(6);
|
||||
for (AssetWorkspaceMutationChange change : changes) {
|
||||
final Label label = new Label(change.verb() + " · " + change.target());
|
||||
label.getStyleClass().add("assets-mutation-change");
|
||||
box.getChildren().add(label);
|
||||
}
|
||||
return box;
|
||||
}
|
||||
|
||||
private Node createMutationMessages(java.util.List<String> messages, String styleClass, String emptyText) {
|
||||
if (messages.isEmpty()) {
|
||||
final Label label = new Label(emptyText);
|
||||
label.setWrapText(true);
|
||||
label.getStyleClass().add("assets-details-section-message");
|
||||
return label;
|
||||
}
|
||||
final VBox box = new VBox(6);
|
||||
for (String message : messages) {
|
||||
final Label label = new Label(message);
|
||||
label.setWrapText(true);
|
||||
label.getStyleClass().add(styleClass);
|
||||
box.getChildren().add(label);
|
||||
}
|
||||
return box;
|
||||
}
|
||||
|
||||
private Node keyValueRow(String key, String value) {
|
||||
final HBox row = new HBox(12);
|
||||
final Label keyLabel = new Label(key);
|
||||
keyLabel.getStyleClass().add("assets-details-key");
|
||||
final Label valueLabel = new Label(value == null || value.isBlank() ? "—" : value);
|
||||
valueLabel.getStyleClass().add("assets-details-value");
|
||||
valueLabel.setWrapText(true);
|
||||
HBox.setHgrow(valueLabel, Priority.ALWAYS);
|
||||
row.getChildren().addAll(keyLabel, valueLabel);
|
||||
return row;
|
||||
}
|
||||
|
||||
private String relativeAssetPath(Path assetRoot) {
|
||||
final Path assetsRoot = AssetRootValidator.assetsRoot(projectReference);
|
||||
final Path normalizedAssetRoot = assetRoot.toAbsolutePath().normalize();
|
||||
if (!normalizedAssetRoot.startsWith(assetsRoot)) {
|
||||
return normalizedAssetRoot.toString();
|
||||
}
|
||||
return assetsRoot.relativize(normalizedAssetRoot).toString().replace('\\', '/');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
public record AssetRootValidationResult(
|
||||
boolean valid,
|
||||
String normalizedRelativeRoot,
|
||||
Path assetRoot,
|
||||
String message) {
|
||||
|
||||
public AssetRootValidationResult {
|
||||
normalizedRelativeRoot = Objects.requireNonNullElse(normalizedRelativeRoot, "");
|
||||
message = Objects.requireNonNullElse(message, "");
|
||||
assetRoot = assetRoot == null ? null : assetRoot.toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
public static AssetRootValidationResult success(String normalizedRelativeRoot, Path assetRoot) {
|
||||
return new AssetRootValidationResult(true, normalizedRelativeRoot, assetRoot, "");
|
||||
}
|
||||
|
||||
public static AssetRootValidationResult failure(String message) {
|
||||
return new AssetRootValidationResult(false, "", null, message);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
import p.packer.api.PackerProjectContext;
|
||||
import p.packer.foundation.PackerWorkspacePaths;
|
||||
import p.studio.projects.ProjectReference;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
final class AssetRootValidator {
|
||||
private static final String ASSET_MANIFEST = "asset.json";
|
||||
private static final String CONTROL_DIR = ".prometeu";
|
||||
|
||||
private AssetRootValidator() {
|
||||
}
|
||||
|
||||
static Path assetsRoot(ProjectReference projectReference) {
|
||||
final ProjectReference reference = Objects.requireNonNull(projectReference, "projectReference");
|
||||
return PackerWorkspacePaths.assetsRoot(new PackerProjectContext(reference.name(), reference.rootPath()));
|
||||
}
|
||||
|
||||
static AssetRootValidationResult validate(ProjectReference projectReference, String relativeRootText) {
|
||||
final Path relativeRoot;
|
||||
try {
|
||||
relativeRoot = Path.of(Objects.requireNonNullElse(relativeRootText, "").trim()).normalize();
|
||||
} catch (RuntimeException runtimeException) {
|
||||
return AssetRootValidationResult.failure("Asset root must be a valid relative path.");
|
||||
}
|
||||
|
||||
if (relativeRootText == null || relativeRootText.isBlank() || relativeRoot.getNameCount() == 0 || relativeRoot.isAbsolute() || relativeRoot.startsWith("..")) {
|
||||
return AssetRootValidationResult.failure("Asset root must stay inside assets/.");
|
||||
}
|
||||
if (CONTROL_DIR.equals(relativeRoot.getName(0).toString())) {
|
||||
return AssetRootValidationResult.failure("Asset root must not target the reserved .prometeu directory.");
|
||||
}
|
||||
|
||||
final Path assetsRoot = assetsRoot(projectReference);
|
||||
final Path assetRoot = assetsRoot.resolve(relativeRoot).toAbsolutePath().normalize();
|
||||
if (!assetRoot.startsWith(assetsRoot)) {
|
||||
return AssetRootValidationResult.failure("Asset root must stay inside assets/.");
|
||||
}
|
||||
if (Files.isRegularFile(assetRoot.resolve(ASSET_MANIFEST))) {
|
||||
return AssetRootValidationResult.failure("asset.json already exists at: " + relativeRoot.toString().replace('\\', '/'));
|
||||
}
|
||||
return AssetRootValidationResult.success(relativeRoot.toString().replace('\\', '/'), assetRoot);
|
||||
}
|
||||
|
||||
static AssetRootValidationResult fromAbsoluteDirectory(ProjectReference projectReference, Path directory) {
|
||||
if (directory == null) {
|
||||
return AssetRootValidationResult.failure("Asset root is required.");
|
||||
}
|
||||
final Path assetsRoot = assetsRoot(projectReference);
|
||||
final Path normalized = directory.toAbsolutePath().normalize();
|
||||
if (!normalized.startsWith(assetsRoot)) {
|
||||
return AssetRootValidationResult.failure("Choose a directory inside assets/.");
|
||||
}
|
||||
return validate(projectReference, assetsRoot.relativize(normalized).toString().replace('\\', '/'));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,12 +1,9 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
public enum AssetWorkspaceAction {
|
||||
DOCTOR,
|
||||
BUILD,
|
||||
ADOPT,
|
||||
REGISTER,
|
||||
QUARANTINE,
|
||||
INCLUDE_IN_BUILD,
|
||||
EXCLUDE_FROM_BUILD,
|
||||
RELOCATE,
|
||||
FORGET,
|
||||
REMOVE
|
||||
}
|
||||
|
||||
@ -10,18 +10,21 @@ public final class AssetWorkspaceActionSetBuilder {
|
||||
public static AssetWorkspaceActionSet forAsset(AssetWorkspaceAssetSummary summary) {
|
||||
Objects.requireNonNull(summary, "summary");
|
||||
return switch (summary.state()) {
|
||||
case MANAGED -> new AssetWorkspaceActionSet(
|
||||
List.of(AssetWorkspaceAction.DOCTOR, AssetWorkspaceAction.BUILD),
|
||||
case REGISTERED -> summary.buildParticipation() == AssetWorkspaceBuildParticipation.INCLUDED
|
||||
? new AssetWorkspaceActionSet(
|
||||
List.of(),
|
||||
List.of(
|
||||
AssetWorkspaceAction.EXCLUDE_FROM_BUILD,
|
||||
AssetWorkspaceAction.RELOCATE,
|
||||
AssetWorkspaceAction.REMOVE))
|
||||
: new AssetWorkspaceActionSet(
|
||||
List.of(AssetWorkspaceAction.INCLUDE_IN_BUILD),
|
||||
List.of(
|
||||
AssetWorkspaceAction.FORGET,
|
||||
AssetWorkspaceAction.QUARANTINE,
|
||||
AssetWorkspaceAction.RELOCATE,
|
||||
AssetWorkspaceAction.REMOVE));
|
||||
case ORPHAN -> new AssetWorkspaceActionSet(
|
||||
List.of(AssetWorkspaceAction.ADOPT, AssetWorkspaceAction.REGISTER),
|
||||
List.of(
|
||||
AssetWorkspaceAction.QUARANTINE,
|
||||
AssetWorkspaceAction.RELOCATE));
|
||||
case UNREGISTERED -> new AssetWorkspaceActionSet(
|
||||
List.of(AssetWorkspaceAction.REGISTER),
|
||||
List.of(AssetWorkspaceAction.RELOCATE));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
public enum AssetWorkspaceAssetState {
|
||||
MANAGED,
|
||||
ORPHAN
|
||||
REGISTERED,
|
||||
UNREGISTERED
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ public record AssetWorkspaceAssetSummary(
|
||||
AssetWorkspaceSelectionKey selectionKey,
|
||||
String assetName,
|
||||
AssetWorkspaceAssetState state,
|
||||
AssetWorkspaceBuildParticipation buildParticipation,
|
||||
Integer assetId,
|
||||
String assetFamily,
|
||||
Path assetRoot,
|
||||
@ -17,13 +18,17 @@ public record AssetWorkspaceAssetSummary(
|
||||
Objects.requireNonNull(selectionKey, "selectionKey");
|
||||
assetName = Objects.requireNonNull(assetName, "assetName").trim();
|
||||
Objects.requireNonNull(state, "state");
|
||||
Objects.requireNonNull(buildParticipation, "buildParticipation");
|
||||
assetFamily = Objects.requireNonNullElse(assetFamily, "unknown").trim();
|
||||
assetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
|
||||
if (assetName.isBlank()) {
|
||||
throw new IllegalArgumentException("assetName must not be blank");
|
||||
}
|
||||
if (state == AssetWorkspaceAssetState.MANAGED && assetId == null) {
|
||||
throw new IllegalArgumentException("managed asset must expose assetId");
|
||||
if (state == AssetWorkspaceAssetState.REGISTERED && assetId == null) {
|
||||
throw new IllegalArgumentException("registered asset must expose assetId");
|
||||
}
|
||||
if (state == AssetWorkspaceAssetState.UNREGISTERED && buildParticipation != AssetWorkspaceBuildParticipation.EXCLUDED) {
|
||||
throw new IllegalArgumentException("unregistered asset must stay excluded from build participation");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
public enum AssetWorkspaceBuildParticipation {
|
||||
INCLUDED,
|
||||
EXCLUDED
|
||||
}
|
||||
@ -2,8 +2,21 @@ package p.studio.workspaces.assets;
|
||||
|
||||
import p.studio.projects.ProjectReference;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public interface AssetWorkspaceMutationService {
|
||||
AssetWorkspaceMutationPreview preview(ProjectReference projectReference, AssetWorkspaceAssetSummary asset, AssetWorkspaceAction action);
|
||||
default AssetWorkspaceMutationPreview preview(
|
||||
ProjectReference projectReference,
|
||||
AssetWorkspaceAssetSummary asset,
|
||||
AssetWorkspaceAction action) {
|
||||
return preview(projectReference, asset, action, null);
|
||||
}
|
||||
|
||||
AssetWorkspaceMutationPreview preview(
|
||||
ProjectReference projectReference,
|
||||
AssetWorkspaceAssetSummary asset,
|
||||
AssetWorkspaceAction action,
|
||||
Path targetRoot);
|
||||
|
||||
void apply(ProjectReference projectReference, AssetWorkspaceMutationPreview preview);
|
||||
}
|
||||
|
||||
@ -15,11 +15,13 @@ import java.util.stream.Stream;
|
||||
public final class FileSystemAssetWorkspaceMutationService implements AssetWorkspaceMutationService {
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
private static final String PROMETEU_DIR = ".prometeu";
|
||||
private static final String QUARANTINE_DIR = "quarantine";
|
||||
private static final String RECOVERED_DIR = "recovered";
|
||||
|
||||
@Override
|
||||
public AssetWorkspaceMutationPreview preview(ProjectReference projectReference, AssetWorkspaceAssetSummary asset, AssetWorkspaceAction action) {
|
||||
public AssetWorkspaceMutationPreview preview(
|
||||
ProjectReference projectReference,
|
||||
AssetWorkspaceAssetSummary asset,
|
||||
AssetWorkspaceAction action,
|
||||
Path targetRoot) {
|
||||
Objects.requireNonNull(projectReference, "projectReference");
|
||||
Objects.requireNonNull(asset, "asset");
|
||||
Objects.requireNonNull(action, "action");
|
||||
@ -37,45 +39,59 @@ public final class FileSystemAssetWorkspaceMutationService implements AssetWorks
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case ADOPT, REGISTER -> {
|
||||
if (asset.state() == AssetWorkspaceAssetState.MANAGED) {
|
||||
blockers.add("Asset is already managed.");
|
||||
case REGISTER -> {
|
||||
if (asset.state() == AssetWorkspaceAssetState.REGISTERED) {
|
||||
blockers.add("Asset is already registered.");
|
||||
} else {
|
||||
changes.add(new AssetWorkspaceMutationChange(
|
||||
AssetWorkspaceMutationChangeScope.REGISTRY,
|
||||
"ADD",
|
||||
relativeRoot));
|
||||
if (asset.hasDiagnostics()) {
|
||||
warnings.add("Asset currently reports diagnostics and will still be adopted.");
|
||||
warnings.add("Asset currently reports diagnostics and will still be registered.");
|
||||
}
|
||||
}
|
||||
}
|
||||
case QUARANTINE -> {
|
||||
if (isInsideQuarantine(asset.assetRoot(), assetsRoot)) {
|
||||
blockers.add("Asset is already inside quarantine.");
|
||||
case INCLUDE_IN_BUILD -> {
|
||||
if (asset.state() != AssetWorkspaceAssetState.REGISTERED) {
|
||||
blockers.add("Only registered assets can be included in builds.");
|
||||
} else if (asset.buildParticipation() == AssetWorkspaceBuildParticipation.INCLUDED) {
|
||||
blockers.add("Asset is already included in builds.");
|
||||
} else {
|
||||
targetAssetRoot = nextAvailablePath(quarantineRoot(assetsRoot), sanitizeSegment(asset.assetName()));
|
||||
if (asset.state() == AssetWorkspaceAssetState.MANAGED) {
|
||||
changes.add(new AssetWorkspaceMutationChange(
|
||||
AssetWorkspaceMutationChangeScope.REGISTRY,
|
||||
"REMOVE",
|
||||
relativeRoot));
|
||||
warnings.add("Quarantining a managed asset removes it from the active registry.");
|
||||
}
|
||||
changes.add(new AssetWorkspaceMutationChange(
|
||||
AssetWorkspaceMutationChangeScope.WORKSPACE,
|
||||
"MOVE",
|
||||
relativeRoot + " -> " + relativeAssetRoot(targetAssetRoot, assetsRoot)));
|
||||
warnings.add("Quarantine is explicit and reversible, but the asset will leave its current workspace location.");
|
||||
AssetWorkspaceMutationChangeScope.REGISTRY,
|
||||
"UPDATE",
|
||||
relativeRoot));
|
||||
warnings.add("The asset will remain registered and return to the build set.");
|
||||
}
|
||||
}
|
||||
case EXCLUDE_FROM_BUILD -> {
|
||||
if (asset.state() != AssetWorkspaceAssetState.REGISTERED) {
|
||||
blockers.add("Only registered assets can be excluded from builds.");
|
||||
} else if (asset.buildParticipation() == AssetWorkspaceBuildParticipation.EXCLUDED) {
|
||||
blockers.add("Asset is already excluded from builds.");
|
||||
} else {
|
||||
changes.add(new AssetWorkspaceMutationChange(
|
||||
AssetWorkspaceMutationChangeScope.REGISTRY,
|
||||
"UPDATE",
|
||||
relativeRoot));
|
||||
warnings.add("The asset will remain registered but leave the build set.");
|
||||
}
|
||||
}
|
||||
case RELOCATE -> {
|
||||
targetAssetRoot = relocationTarget(asset, assetsRoot);
|
||||
final String targetRelativeRoot = relativeAssetRoot(targetAssetRoot, assetsRoot);
|
||||
if (asset.assetRoot().equals(targetAssetRoot)) {
|
||||
blockers.add("Asset is already at the planned relocation target.");
|
||||
targetAssetRoot = targetRoot == null
|
||||
? relocationTarget(asset, assetsRoot)
|
||||
: targetRoot.toAbsolutePath().normalize();
|
||||
final AssetRootValidationResult validation = AssetRelocationTargetValidator.validate(
|
||||
projectReference,
|
||||
asset.assetRoot(),
|
||||
targetAssetRoot);
|
||||
if (!validation.valid()) {
|
||||
blockers.add(validation.message());
|
||||
} else {
|
||||
if (asset.state() == AssetWorkspaceAssetState.MANAGED) {
|
||||
targetAssetRoot = validation.assetRoot();
|
||||
final String targetRelativeRoot = validation.normalizedRelativeRoot();
|
||||
if (asset.state() == AssetWorkspaceAssetState.REGISTERED) {
|
||||
changes.add(new AssetWorkspaceMutationChange(
|
||||
AssetWorkspaceMutationChangeScope.REGISTRY,
|
||||
"UPDATE",
|
||||
@ -88,19 +104,8 @@ public final class FileSystemAssetWorkspaceMutationService implements AssetWorks
|
||||
warnings.add("Relocation preserves asset identity, but it changes the root path seen by the workspace.");
|
||||
}
|
||||
}
|
||||
case FORGET -> {
|
||||
if (asset.state() != AssetWorkspaceAssetState.MANAGED) {
|
||||
blockers.add("Only managed assets can be forgotten.");
|
||||
} else {
|
||||
changes.add(new AssetWorkspaceMutationChange(
|
||||
AssetWorkspaceMutationChangeScope.REGISTRY,
|
||||
"REMOVE",
|
||||
relativeRoot));
|
||||
warnings.add("The asset will leave the managed build set.");
|
||||
}
|
||||
}
|
||||
case REMOVE -> {
|
||||
if (asset.state() == AssetWorkspaceAssetState.MANAGED) {
|
||||
if (asset.state() == AssetWorkspaceAssetState.REGISTERED) {
|
||||
changes.add(new AssetWorkspaceMutationChange(
|
||||
AssetWorkspaceMutationChangeScope.REGISTRY,
|
||||
"REMOVE",
|
||||
@ -112,7 +117,6 @@ public final class FileSystemAssetWorkspaceMutationService implements AssetWorks
|
||||
relativeRoot));
|
||||
warnings.add("Physical files inside the asset root will be deleted.");
|
||||
}
|
||||
case DOCTOR, BUILD -> safeFixes.add("This action is handled outside the staged mutation flow.");
|
||||
}
|
||||
|
||||
final boolean highRisk = action == AssetWorkspaceAction.REMOVE || action == AssetWorkspaceAction.RELOCATE;
|
||||
@ -132,7 +136,7 @@ public final class FileSystemAssetWorkspaceMutationService implements AssetWorks
|
||||
final Path assetRoot = preview.asset().assetRoot();
|
||||
|
||||
switch (preview.action()) {
|
||||
case ADOPT, REGISTER -> {
|
||||
case REGISTER -> {
|
||||
if (registryContainsRoot(registry, assetRoot, assetsRoot)) {
|
||||
return;
|
||||
}
|
||||
@ -145,19 +149,17 @@ public final class FileSystemAssetWorkspaceMutationService implements AssetWorks
|
||||
registry.nextAssetId = entry.assetId + 1;
|
||||
writeRegistry(assetsRoot, registry);
|
||||
}
|
||||
case FORGET -> {
|
||||
registry.assets.removeIf(entry -> entry.root != null && assetsRoot.resolve(entry.root).toAbsolutePath().normalize().equals(assetRoot));
|
||||
case INCLUDE_IN_BUILD, EXCLUDE_FROM_BUILD -> {
|
||||
final boolean includedInBuild = preview.action() == AssetWorkspaceAction.INCLUDE_IN_BUILD;
|
||||
registry.assets.stream()
|
||||
.filter(entry -> entry.root != null && assetsRoot.resolve(entry.root).toAbsolutePath().normalize().equals(assetRoot))
|
||||
.findFirst()
|
||||
.ifPresent(entry -> entry.includedInBuild = includedInBuild);
|
||||
writeRegistry(assetsRoot, registry);
|
||||
}
|
||||
case QUARANTINE -> {
|
||||
final Path targetAssetRoot = requireTargetAssetRoot(preview);
|
||||
registry.assets.removeIf(entry -> entry.root != null && assetsRoot.resolve(entry.root).toAbsolutePath().normalize().equals(assetRoot));
|
||||
writeRegistry(assetsRoot, registry);
|
||||
moveAssetRoot(assetRoot, targetAssetRoot);
|
||||
}
|
||||
case RELOCATE -> {
|
||||
final Path targetAssetRoot = requireTargetAssetRoot(preview);
|
||||
if (preview.asset().state() == AssetWorkspaceAssetState.MANAGED) {
|
||||
if (preview.asset().state() == AssetWorkspaceAssetState.REGISTERED) {
|
||||
registry.assets.stream()
|
||||
.filter(entry -> entry.root != null && assetsRoot.resolve(entry.root).toAbsolutePath().normalize().equals(assetRoot))
|
||||
.findFirst()
|
||||
@ -171,8 +173,6 @@ public final class FileSystemAssetWorkspaceMutationService implements AssetWorks
|
||||
writeRegistry(assetsRoot, registry);
|
||||
deleteRecursively(assetRoot);
|
||||
}
|
||||
case DOCTOR, BUILD -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -223,34 +223,12 @@ public final class FileSystemAssetWorkspaceMutationService implements AssetWorks
|
||||
return assetsRoot.relativize(assetRoot.toAbsolutePath().normalize()).toString().replace('\\', '/');
|
||||
}
|
||||
|
||||
private Path quarantineRoot(Path assetsRoot) {
|
||||
return assetsRoot.resolve(PROMETEU_DIR).resolve(QUARANTINE_DIR).toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
private boolean isInsideQuarantine(Path assetRoot, Path assetsRoot) {
|
||||
return assetRoot.toAbsolutePath().normalize().startsWith(quarantineRoot(assetsRoot));
|
||||
}
|
||||
|
||||
private Path relocationTarget(AssetWorkspaceAssetSummary asset, Path assetsRoot) {
|
||||
final Path assetRoot = asset.assetRoot();
|
||||
if (isInsideQuarantine(assetRoot, assetsRoot)) {
|
||||
return nextAvailablePath(assetsRoot.resolve(RECOVERED_DIR), sanitizeSegment(asset.assetName()));
|
||||
}
|
||||
final Path siblingParent = assetRoot.getParent() == null ? assetsRoot : assetRoot.getParent();
|
||||
return nextAvailableSibling(siblingParent, assetRoot.getFileName().toString() + "-relocated");
|
||||
}
|
||||
|
||||
private Path nextAvailablePath(Path parent, String baseName) {
|
||||
final Path normalizedParent = parent.toAbsolutePath().normalize();
|
||||
Path candidate = normalizedParent.resolve(baseName);
|
||||
int index = 2;
|
||||
while (Files.exists(candidate)) {
|
||||
candidate = normalizedParent.resolve(baseName + "-" + index);
|
||||
index += 1;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private Path nextAvailableSibling(Path parent, String baseName) {
|
||||
final Path normalizedParent = parent.toAbsolutePath().normalize();
|
||||
Path candidate = normalizedParent.resolve(baseName);
|
||||
@ -262,16 +240,6 @@ public final class FileSystemAssetWorkspaceMutationService implements AssetWorks
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private String sanitizeSegment(String value) {
|
||||
final String sanitized = value == null
|
||||
? "asset"
|
||||
: value.trim()
|
||||
.replaceAll("[^A-Za-z0-9._-]+", "-")
|
||||
.replaceAll("-{2,}", "-")
|
||||
.replaceAll("^[.-]+|[.-]+$", "");
|
||||
return sanitized == null || sanitized.isBlank() ? "asset" : sanitized;
|
||||
}
|
||||
|
||||
private Path requireTargetAssetRoot(AssetWorkspaceMutationPreview preview) {
|
||||
if (preview.targetAssetRoot() == null) {
|
||||
throw new IllegalStateException("Mutation preview does not define a target asset root");
|
||||
@ -330,5 +298,8 @@ public final class FileSystemAssetWorkspaceMutationService implements AssetWorks
|
||||
|
||||
@JsonProperty("root")
|
||||
public String root;
|
||||
|
||||
@JsonProperty("included_in_build")
|
||||
public boolean includedInBuild = true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ public final class FileSystemAssetWorkspaceService implements AssetWorkspaceServ
|
||||
return new AssetWorkspaceSnapshot(List.of());
|
||||
}
|
||||
|
||||
final Map<Path, Integer> registryByRoot = readRegistry(assetsRoot);
|
||||
final Map<Path, RegistryEntry> registryByRoot = readRegistry(assetsRoot);
|
||||
try (Stream<Path> paths = Files.find(assetsRoot, Integer.MAX_VALUE, (path, attrs) ->
|
||||
attrs.isRegularFile() && path.getFileName().toString().equals("asset.json"))) {
|
||||
final List<AssetWorkspaceAssetSummary> assets = paths
|
||||
@ -42,7 +42,7 @@ public final class FileSystemAssetWorkspaceService implements AssetWorkspaceServ
|
||||
public AssetWorkspaceAssetDetails loadAssetDetails(ProjectReference projectReference, AssetWorkspaceSelectionKey selectionKey) {
|
||||
final Path projectRoot = Objects.requireNonNull(projectReference, "projectReference").rootPath();
|
||||
final Path assetsRoot = projectRoot.resolve("assets");
|
||||
final Map<Path, Integer> registryByRoot = readRegistry(assetsRoot);
|
||||
final Map<Path, RegistryEntry> registryByRoot = readRegistry(assetsRoot);
|
||||
final Path assetRoot = resolveAssetRoot(selectionKey, assetsRoot, registryByRoot);
|
||||
final Path assetManifestPath = assetRoot.resolve("asset.json");
|
||||
try {
|
||||
@ -65,14 +65,15 @@ public final class FileSystemAssetWorkspaceService implements AssetWorkspaceServ
|
||||
}
|
||||
}
|
||||
|
||||
private AssetWorkspaceAssetSummary buildAssetSummary(Path assetManifestPath, Map<Path, Integer> registryByRoot) {
|
||||
private AssetWorkspaceAssetSummary buildAssetSummary(Path assetManifestPath, Map<Path, RegistryEntry> registryByRoot) {
|
||||
final Path assetRoot = assetManifestPath.getParent().toAbsolutePath().normalize();
|
||||
try {
|
||||
final AssetManifest manifest = MAPPER.readValue(assetManifestPath.toFile(), AssetManifest.class);
|
||||
final Integer assetId = registryByRoot.get(assetRoot);
|
||||
final RegistryEntry registryEntry = registryByRoot.get(assetRoot);
|
||||
final Integer assetId = registryEntry == null ? null : registryEntry.assetId();
|
||||
final AssetWorkspaceAssetState state = assetId == null
|
||||
? AssetWorkspaceAssetState.ORPHAN
|
||||
: AssetWorkspaceAssetState.MANAGED;
|
||||
? AssetWorkspaceAssetState.UNREGISTERED
|
||||
: AssetWorkspaceAssetState.REGISTERED;
|
||||
final AssetWorkspaceSelectionKey selectionKey = assetId == null
|
||||
? new AssetWorkspaceSelectionKey.OrphanAsset(assetRoot)
|
||||
: new AssetWorkspaceSelectionKey.ManagedAsset(assetId);
|
||||
@ -87,6 +88,9 @@ public final class FileSystemAssetWorkspaceService implements AssetWorkspaceServ
|
||||
selectionKey,
|
||||
assetName,
|
||||
state,
|
||||
state == AssetWorkspaceAssetState.REGISTERED
|
||||
? (registryEntry.includedInBuild() ? AssetWorkspaceBuildParticipation.INCLUDED : AssetWorkspaceBuildParticipation.EXCLUDED)
|
||||
: AssetWorkspaceBuildParticipation.EXCLUDED,
|
||||
assetId,
|
||||
assetFamily,
|
||||
assetRoot,
|
||||
@ -97,7 +101,8 @@ public final class FileSystemAssetWorkspaceService implements AssetWorkspaceServ
|
||||
return new AssetWorkspaceAssetSummary(
|
||||
selectionKey,
|
||||
assetRoot.getFileName().toString(),
|
||||
AssetWorkspaceAssetState.ORPHAN,
|
||||
AssetWorkspaceAssetState.UNREGISTERED,
|
||||
AssetWorkspaceBuildParticipation.EXCLUDED,
|
||||
null,
|
||||
"unknown",
|
||||
assetRoot,
|
||||
@ -106,7 +111,7 @@ public final class FileSystemAssetWorkspaceService implements AssetWorkspaceServ
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Path, Integer> readRegistry(Path assetsRoot) {
|
||||
private Map<Path, RegistryEntry> readRegistry(Path assetsRoot) {
|
||||
final Path registryPath = assetsRoot.resolve(".prometeu").resolve("index.json");
|
||||
if (!Files.isRegularFile(registryPath)) {
|
||||
return Map.of();
|
||||
@ -117,12 +122,12 @@ public final class FileSystemAssetWorkspaceService implements AssetWorkspaceServ
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
final Map<Path, Integer> registryByRoot = new HashMap<>();
|
||||
final Map<Path, RegistryEntry> registryByRoot = new HashMap<>();
|
||||
for (RegistryEntry entry : registry.assets()) {
|
||||
if (entry == null || entry.root() == null || entry.root().isBlank() || entry.assetId() == null) {
|
||||
continue;
|
||||
}
|
||||
registryByRoot.put(assetsRoot.resolve(entry.root()).toAbsolutePath().normalize(), entry.assetId());
|
||||
registryByRoot.put(assetsRoot.resolve(entry.root()).toAbsolutePath().normalize(), entry);
|
||||
}
|
||||
return Map.copyOf(registryByRoot);
|
||||
} catch (IOException ioException) {
|
||||
@ -133,10 +138,10 @@ public final class FileSystemAssetWorkspaceService implements AssetWorkspaceServ
|
||||
private Path resolveAssetRoot(
|
||||
AssetWorkspaceSelectionKey selectionKey,
|
||||
Path assetsRoot,
|
||||
Map<Path, Integer> registryByRoot) {
|
||||
Map<Path, RegistryEntry> registryByRoot) {
|
||||
return switch (selectionKey) {
|
||||
case AssetWorkspaceSelectionKey.ManagedAsset managedAsset -> registryByRoot.entrySet().stream()
|
||||
.filter(entry -> managedAsset.assetId() == entry.getValue())
|
||||
.filter(entry -> managedAsset.assetId() == entry.getValue().assetId())
|
||||
.map(Map.Entry::getKey)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("managed asset root not found for assetId " + managedAsset.assetId()));
|
||||
@ -188,12 +193,12 @@ public final class FileSystemAssetWorkspaceService implements AssetWorkspaceServ
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private record RegistryEntry(Integer assetId, String root) {
|
||||
private record RegistryEntry(Integer assetId, String root, boolean includedInBuild) {
|
||||
private RegistryEntry(
|
||||
@com.fasterxml.jackson.annotation.JsonProperty("asset_id") Integer assetId,
|
||||
@com.fasterxml.jackson.annotation.JsonProperty("root") String root) {
|
||||
this.assetId = assetId;
|
||||
this.root = root;
|
||||
@com.fasterxml.jackson.annotation.JsonProperty("root") String root,
|
||||
@com.fasterxml.jackson.annotation.JsonProperty("included_in_build") Boolean includedInBuild) {
|
||||
this(assetId, root, includedInBuild == null || includedInBuild);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,84 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import p.packer.api.PackerProjectContext;
|
||||
import p.packer.api.workspace.InitWorkspaceRequest;
|
||||
import p.packer.foundation.PackerRegistryState;
|
||||
import p.packer.foundation.PackerWorkspaceFoundation;
|
||||
import p.studio.projects.ProjectReference;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class PackerBackedAssetCreationService implements AssetCreationService {
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
private static final String ASSET_MANIFEST = "asset.json";
|
||||
|
||||
private final PackerWorkspaceFoundation workspaceFoundation;
|
||||
|
||||
public PackerBackedAssetCreationService() {
|
||||
this(new PackerWorkspaceFoundation());
|
||||
}
|
||||
|
||||
PackerBackedAssetCreationService(PackerWorkspaceFoundation workspaceFoundation) {
|
||||
this.workspaceFoundation = Objects.requireNonNull(workspaceFoundation, "workspaceFoundation");
|
||||
}
|
||||
|
||||
@Override
|
||||
public AssetCreationResult create(ProjectReference projectReference, AssetCreationRequest request) {
|
||||
final ProjectReference reference = Objects.requireNonNull(projectReference, "projectReference");
|
||||
final AssetCreationRequest creation = Objects.requireNonNull(request, "request");
|
||||
final PackerProjectContext project = new PackerProjectContext(reference.name(), reference.rootPath());
|
||||
workspaceFoundation.initWorkspace(new InitWorkspaceRequest(project));
|
||||
|
||||
final AssetRootValidationResult rootValidation = AssetRootValidator.validate(reference, creation.relativeRoot());
|
||||
if (!rootValidation.valid()) {
|
||||
throw new IllegalArgumentException(rootValidation.message());
|
||||
}
|
||||
if (!AssetCreationCatalog.supports(creation.assetType(), creation.outputFormat(), creation.outputCodec())) {
|
||||
throw new IllegalArgumentException("Selected asset type, output format, and codec combination is not supported.");
|
||||
}
|
||||
final Path assetRoot = rootValidation.assetRoot();
|
||||
final Path manifestPath = assetRoot.resolve(ASSET_MANIFEST);
|
||||
final PackerRegistryState registry = workspaceFoundation.loadRegistry(project);
|
||||
if (workspaceFoundation.lookup().findByRoot(project, registry, assetRoot).isPresent()) {
|
||||
throw new IllegalArgumentException("Asset root is already registered in the project: " + rootValidation.normalizedRelativeRoot());
|
||||
}
|
||||
if (Files.isRegularFile(manifestPath)) {
|
||||
throw new IllegalArgumentException("asset.json already exists at: " + rootValidation.normalizedRelativeRoot());
|
||||
}
|
||||
|
||||
try {
|
||||
Files.createDirectories(assetRoot);
|
||||
MAPPER.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifestDocument(creation));
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException("Unable to create asset files: " + exception.getMessage(), exception);
|
||||
}
|
||||
|
||||
final var entry = workspaceFoundation.allocateIdentity(project, registry, assetRoot);
|
||||
workspaceFoundation.saveRegistry(project, workspaceFoundation.appendAllocatedEntry(registry, entry));
|
||||
return new AssetCreationResult(new AssetWorkspaceSelectionKey.ManagedAsset(entry.assetId()), assetRoot);
|
||||
}
|
||||
|
||||
private Map<String, Object> manifestDocument(AssetCreationRequest request) {
|
||||
final Map<String, Object> root = new LinkedHashMap<>();
|
||||
root.put("schema_version", 1);
|
||||
root.put("name", request.assetName());
|
||||
root.put("type", request.assetType());
|
||||
root.put("inputs", Map.of());
|
||||
|
||||
final Map<String, Object> output = new LinkedHashMap<>();
|
||||
output.put("format", request.outputFormat());
|
||||
output.put("codec", request.outputCodec());
|
||||
root.put("output", output);
|
||||
|
||||
final Map<String, Object> preload = new LinkedHashMap<>();
|
||||
preload.put("enabled", request.preloadEnabled());
|
||||
root.put("preload", preload);
|
||||
return root;
|
||||
}
|
||||
}
|
||||
@ -48,15 +48,34 @@ public final class PackerBackedAssetWorkspaceMutationService implements AssetWor
|
||||
}
|
||||
|
||||
@Override
|
||||
public AssetWorkspaceMutationPreview preview(ProjectReference projectReference, AssetWorkspaceAssetSummary asset, AssetWorkspaceAction action) {
|
||||
public AssetWorkspaceMutationPreview preview(
|
||||
ProjectReference projectReference,
|
||||
AssetWorkspaceAssetSummary asset,
|
||||
AssetWorkspaceAction action,
|
||||
Path targetRoot) {
|
||||
Objects.requireNonNull(projectReference, "projectReference");
|
||||
Objects.requireNonNull(asset, "asset");
|
||||
final Path validatedTargetRoot = validateTargetRoot(projectReference, asset, action, targetRoot);
|
||||
if (action == AssetWorkspaceAction.RELOCATE && targetRoot != null && validatedTargetRoot == null) {
|
||||
final AssetRootValidationResult validation = AssetRelocationTargetValidator.validate(projectReference, asset.assetRoot(), targetRoot);
|
||||
final AssetWorkspaceMutationPreview preview = new AssetWorkspaceMutationPreview(
|
||||
action,
|
||||
asset,
|
||||
List.of(validation.message()),
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of(),
|
||||
true,
|
||||
targetRoot);
|
||||
eventBus.publish(new StudioAssetsMutationPreviewReadyEvent(projectReference, action, 1));
|
||||
return preview;
|
||||
}
|
||||
final PackerMutationPreview packerPreview = packerMutationService.preview(
|
||||
new PackerMutationRequest(
|
||||
project(projectReference),
|
||||
mutationType(action),
|
||||
assetReference(projectReference, asset),
|
||||
null));
|
||||
validatedTargetRoot));
|
||||
final AssetWorkspaceMutationPreview studioPreview = mapPreview(action, asset, packerPreview);
|
||||
final OperationSession session = new OperationSession(projectReference, action, studioPreview, packerPreview);
|
||||
previewSessions.put(studioPreview, session);
|
||||
@ -135,13 +154,11 @@ public final class PackerBackedAssetWorkspaceMutationService implements AssetWor
|
||||
|
||||
private PackerMutationType mutationType(AssetWorkspaceAction action) {
|
||||
return switch (Objects.requireNonNull(action, "action")) {
|
||||
case ADOPT -> PackerMutationType.ADOPT_ASSET;
|
||||
case REGISTER -> PackerMutationType.REGISTER_ASSET;
|
||||
case QUARANTINE -> PackerMutationType.QUARANTINE_ASSET;
|
||||
case INCLUDE_IN_BUILD -> PackerMutationType.INCLUDE_ASSET_IN_BUILD;
|
||||
case EXCLUDE_FROM_BUILD -> PackerMutationType.EXCLUDE_ASSET_FROM_BUILD;
|
||||
case RELOCATE -> PackerMutationType.RELOCATE_ASSET;
|
||||
case FORGET -> PackerMutationType.FORGET_ASSET;
|
||||
case REMOVE -> PackerMutationType.REMOVE_ASSET;
|
||||
case DOCTOR, BUILD -> throw new IllegalArgumentException("Action is not supported by the staged mutation flow: " + action);
|
||||
};
|
||||
}
|
||||
|
||||
@ -161,6 +178,18 @@ public final class PackerBackedAssetWorkspaceMutationService implements AssetWor
|
||||
return event.affectedAssets().isEmpty() ? 1 : event.affectedAssets().size();
|
||||
}
|
||||
|
||||
private Path validateTargetRoot(
|
||||
ProjectReference projectReference,
|
||||
AssetWorkspaceAssetSummary asset,
|
||||
AssetWorkspaceAction action,
|
||||
Path targetRoot) {
|
||||
if (targetRoot == null || action != AssetWorkspaceAction.RELOCATE) {
|
||||
return targetRoot;
|
||||
}
|
||||
final AssetRootValidationResult validation = AssetRelocationTargetValidator.validate(projectReference, asset.assetRoot(), targetRoot);
|
||||
return validation.valid() ? validation.assetRoot() : null;
|
||||
}
|
||||
|
||||
private record OperationSession(
|
||||
ProjectReference projectReference,
|
||||
AssetWorkspaceAction action,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
import p.packer.api.PackerProjectContext;
|
||||
import p.packer.api.assets.PackerBuildParticipation;
|
||||
import p.packer.api.assets.PackerAssetDetails;
|
||||
import p.packer.api.assets.PackerAssetState;
|
||||
import p.packer.api.assets.PackerAssetSummary;
|
||||
@ -47,13 +48,14 @@ public final class PackerBackedAssetWorkspaceService implements AssetWorkspaceSe
|
||||
|
||||
private AssetWorkspaceAssetSummary mapSummary(PackerAssetSummary summary) {
|
||||
final AssetWorkspaceAssetState state = toStudioState(summary);
|
||||
final AssetWorkspaceSelectionKey selectionKey = state == AssetWorkspaceAssetState.MANAGED
|
||||
final AssetWorkspaceSelectionKey selectionKey = state == AssetWorkspaceAssetState.REGISTERED
|
||||
? new AssetWorkspaceSelectionKey.ManagedAsset(summary.identity().assetId())
|
||||
: new AssetWorkspaceSelectionKey.OrphanAsset(summary.identity().assetRoot());
|
||||
return new AssetWorkspaceAssetSummary(
|
||||
selectionKey,
|
||||
summary.identity().assetName(),
|
||||
state,
|
||||
toBuildParticipation(summary.buildParticipation()),
|
||||
summary.identity().assetId(),
|
||||
summary.assetFamily(),
|
||||
summary.identity().assetRoot(),
|
||||
@ -81,10 +83,17 @@ public final class PackerBackedAssetWorkspaceService implements AssetWorkspaceSe
|
||||
}
|
||||
|
||||
private AssetWorkspaceAssetState toStudioState(PackerAssetSummary summary) {
|
||||
if (summary.state() == PackerAssetState.MANAGED) {
|
||||
return AssetWorkspaceAssetState.MANAGED;
|
||||
if (summary.state() == PackerAssetState.REGISTERED) {
|
||||
return AssetWorkspaceAssetState.REGISTERED;
|
||||
}
|
||||
return summary.identity().assetId() != null ? AssetWorkspaceAssetState.MANAGED : AssetWorkspaceAssetState.ORPHAN;
|
||||
return AssetWorkspaceAssetState.UNREGISTERED;
|
||||
}
|
||||
|
||||
private AssetWorkspaceBuildParticipation toBuildParticipation(PackerBuildParticipation buildParticipation) {
|
||||
return switch (buildParticipation) {
|
||||
case INCLUDED -> AssetWorkspaceBuildParticipation.INCLUDED;
|
||||
case EXCLUDED -> AssetWorkspaceBuildParticipation.EXCLUDED;
|
||||
};
|
||||
}
|
||||
|
||||
private PackerProjectContext project(ProjectReference projectReference) {
|
||||
|
||||
@ -53,6 +53,7 @@ public class BuilderWorkspace implements Workspace {
|
||||
|
||||
private ToolBar buildToolBar() {
|
||||
buildButton.textProperty().bind(Container.i18n().bind(I18n.WORKSPACE_SHIPPER_BUTTON_RUN));
|
||||
buildButton.getStyleClass().addAll("studio-button", "studio-button-primary");
|
||||
buildButton.setOnAction(e -> {
|
||||
logs.clear();
|
||||
final var logAggregator = LogAggregator.with(logs::appendText);
|
||||
@ -61,6 +62,7 @@ public class BuilderWorkspace implements Workspace {
|
||||
});
|
||||
|
||||
clearButton.textProperty().bind(Container.i18n().bind(I18n.WORKSPACE_SHIPPER_BUTTON_CLEAR));
|
||||
clearButton.getStyleClass().addAll("studio-button", "studio-button-secondary");
|
||||
clearButton.setOnAction(e -> logs.clear());
|
||||
|
||||
return new ToolBar(buildButton, clearButton);
|
||||
|
||||
@ -17,7 +17,8 @@ public final class EditorToolbar extends HBox {
|
||||
Button saveBtn = iconButton("💾", "Save");
|
||||
|
||||
Button runBtn = iconButton("▶", "Run");
|
||||
runBtn.getStyleClass().add("accent");
|
||||
runBtn.getStyleClass().remove("studio-button-secondary");
|
||||
runBtn.getStyleClass().add("studio-button-primary");
|
||||
|
||||
Region spacer = new Region();
|
||||
HBox.setHgrow(spacer, javafx.scene.layout.Priority.ALWAYS);
|
||||
@ -34,7 +35,7 @@ public final class EditorToolbar extends HBox {
|
||||
private Button iconButton(String icon, String tooltip) {
|
||||
Button b = new Button(icon);
|
||||
b.setFocusTraversable(false);
|
||||
b.getStyleClass().add("toolbar-button");
|
||||
b.getStyleClass().addAll("studio-button", "studio-button-secondary", "studio-button-icon");
|
||||
return b;
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,19 +69,22 @@ workspace.shipper.button.clear=Clear
|
||||
|
||||
workspace.assets=Assets
|
||||
assets.navigator.title=Asset Navigator
|
||||
assets.navigator.action.add=Add Asset
|
||||
assets.navigator.action.doctor=Doctor
|
||||
assets.navigator.action.pack=Pack
|
||||
assets.details.title=Selected Asset
|
||||
assets.search.prompt=Search assets by name or path
|
||||
assets.filter.managed=Managed
|
||||
assets.filter.orphan=Orphan
|
||||
assets.filter.registered=Registered
|
||||
assets.filter.unregistered=Unregistered
|
||||
assets.filter.diagnostics=Diagnostics
|
||||
assets.filter.preload=Preload
|
||||
assets.state.loading=Loading assets...
|
||||
assets.state.empty=No managed or orphan assets were found in this project.
|
||||
assets.state.empty=No registered or unregistered assets were found in this project.
|
||||
assets.state.noResults=No assets match the current search or filters.
|
||||
assets.state.ready={0} visible assets ({1} total).
|
||||
assets.state.error=Asset workspace failed to load.
|
||||
assets.badge.managed=Managed
|
||||
assets.badge.orphan=Orphan
|
||||
assets.badge.registered=Registered
|
||||
assets.badge.unregistered=Unregistered
|
||||
assets.badge.preload=Preload
|
||||
assets.badge.diagnostics=Diagnostics
|
||||
assets.section.summary=Summary
|
||||
@ -89,15 +92,10 @@ assets.section.runtimeContract=Runtime Contract
|
||||
assets.section.inputsPreview=Inputs / Preview
|
||||
assets.section.diagnostics=Diagnostics
|
||||
assets.section.actions=Actions
|
||||
assets.actions.primary=Primary Actions
|
||||
assets.actions.sensitive=Sensitive Actions
|
||||
assets.action.doctor=Doctor
|
||||
assets.action.build=Build
|
||||
assets.action.adopt=Adopt
|
||||
assets.action.register=Register
|
||||
assets.action.quarantine=Quarantine
|
||||
assets.action.includeInBuild=Include In Build
|
||||
assets.action.excludeFromBuild=Exclude From Build
|
||||
assets.action.relocate=Relocate
|
||||
assets.action.forget=Forget
|
||||
assets.action.remove=Remove
|
||||
assets.mutation.previewTitle=Preview: {0}
|
||||
assets.mutation.section.changes=Changes
|
||||
@ -115,17 +113,22 @@ assets.mutation.empty.warnings=No warnings.
|
||||
assets.mutation.empty.safeFixes=No safe fixes.
|
||||
assets.mutation.cancel=Cancel
|
||||
assets.mutation.apply=Apply
|
||||
assets.mutation.confirm.title=Confirm High-Risk Mutation
|
||||
assets.mutation.confirm.title=Confirm Mutation
|
||||
assets.mutation.confirm.header=Confirm {0}
|
||||
assets.mutation.confirm.body=This mutation is marked as high risk and may change or delete workspace files.
|
||||
assets.label.name=Name
|
||||
assets.label.state=State
|
||||
assets.label.registration=Registration
|
||||
assets.label.buildParticipation=Build Participation
|
||||
assets.label.assetId=Asset ID
|
||||
assets.label.type=Type
|
||||
assets.label.location=Location
|
||||
assets.label.targetLocation=Target Location
|
||||
assets.label.format=Format
|
||||
assets.label.codec=Codec
|
||||
assets.label.preload=Preload
|
||||
assets.value.registered=Registered
|
||||
assets.value.unregistered=Unregistered
|
||||
assets.value.included=Included
|
||||
assets.value.excluded=Excluded
|
||||
assets.value.yes=Yes
|
||||
assets.value.no=No
|
||||
assets.progress.idle=Assets workspace idle.
|
||||
@ -150,4 +153,47 @@ assets.details.loading=Waiting for asset data...
|
||||
assets.details.empty=Create or add assets to this project to start using the Assets workspace.
|
||||
assets.details.ready=Selected asset: {0}\nState: {1}\nRoot: {2}
|
||||
assets.details.noSelection=Select an asset from the navigator once assets are available.
|
||||
assets.addWizard.title=Add Asset
|
||||
assets.addWizard.description=Create a registered asset root through a guided flow.
|
||||
assets.addWizard.step.root.title=Choose Asset Root
|
||||
assets.addWizard.step.root.description=Pick a directory inside assets/. Existing roots that already contain asset.json cannot be reused.
|
||||
assets.addWizard.step.details.title=Describe Asset Contract
|
||||
assets.addWizard.step.details.description=Choose the asset family first. Output format and codec are unlocked progressively from that choice.
|
||||
assets.addWizard.step.summary.title=Review and Confirm
|
||||
assets.addWizard.step.summary.description=Confirm the registered asset you are about to create and choose whether it should preload at startup.
|
||||
assets.addWizard.label.name=Asset Name
|
||||
assets.addWizard.label.root=Asset Root
|
||||
assets.addWizard.label.type=Asset Type
|
||||
assets.addWizard.label.format=Output Format
|
||||
assets.addWizard.label.codec=Output Codec
|
||||
assets.addWizard.label.preload=Preload on startup
|
||||
assets.addWizard.prompt.type=Choose asset type
|
||||
assets.addWizard.prompt.format=Choose output format
|
||||
assets.addWizard.prompt.codec=Choose output codec
|
||||
assets.addWizard.assetsRootHint=Assets root: {0}
|
||||
assets.addWizard.browse.title=Choose Asset Root Directory
|
||||
assets.addWizard.note=This preload flag can be changed later by editing asset.json. Inputs start empty and can be added after creation.
|
||||
assets.addWizard.button.create=Create Asset
|
||||
assets.addWizard.error.name=Asset name is required.
|
||||
assets.addWizard.error.root=Asset root is required.
|
||||
assets.addWizard.error.type=Choose an asset type before continuing.
|
||||
assets.addWizard.error.format=Choose an output format before continuing.
|
||||
assets.addWizard.error.codec=Choose an output codec before continuing.
|
||||
assets.addWizard.error.unsupportedCombination=The selected asset type, output format, and codec combination is not supported.
|
||||
assets.relocateWizard.title=Relocate Asset
|
||||
assets.relocateWizard.description=Choose the destination for this asset and confirm the relocation inside this modal.
|
||||
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.label.currentRoot=Current Asset Root
|
||||
assets.relocateWizard.label.destinationParent=Destination Parent
|
||||
assets.relocateWizard.label.destinationName=Destination Folder Name
|
||||
assets.relocateWizard.label.targetRoot=Planned Target Root
|
||||
assets.relocateWizard.prompt.destinationName=Choose the new asset folder name
|
||||
assets.relocateWizard.assetsRootHint=Destination must stay inside assets/: {0}
|
||||
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
|
||||
workspace.debug=Debug
|
||||
|
||||
@ -26,21 +26,6 @@
|
||||
-fx-border-width: 0 1 0 0;
|
||||
}
|
||||
|
||||
.studio-workspace-rail-button {
|
||||
-fx-font-size: 16px;
|
||||
-fx-background-color: transparent;
|
||||
-fx-text-fill: #d4d4d4;
|
||||
-fx-background-radius: 8;
|
||||
}
|
||||
|
||||
.studio-workspace-rail-button:hover {
|
||||
-fx-background-color: #2a2d2e;
|
||||
}
|
||||
|
||||
.studio-workspace-rail-button:selected {
|
||||
-fx-background-color: #37373d;
|
||||
}
|
||||
|
||||
.studio-right-utility-panel {
|
||||
-fx-background-color: #1f1f1f;
|
||||
-fx-border-color: #2d2d2d;
|
||||
@ -108,6 +93,113 @@
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.studio-button {
|
||||
-fx-background-radius: 10;
|
||||
-fx-border-radius: 10;
|
||||
-fx-border-width: 1;
|
||||
-fx-padding: 8 12 8 12;
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
|
||||
.studio-button:disabled {
|
||||
-fx-opacity: 0.45;
|
||||
}
|
||||
|
||||
.studio-button.studio-button-pill {
|
||||
-fx-background-radius: 999;
|
||||
-fx-border-radius: 999;
|
||||
}
|
||||
|
||||
.studio-button.studio-button-icon {
|
||||
-fx-padding: 8;
|
||||
}
|
||||
|
||||
.studio-button.studio-button-primary {
|
||||
-fx-background-color: #27507a;
|
||||
-fx-border-color: #5ea0de;
|
||||
-fx-text-fill: #f7fbff;
|
||||
}
|
||||
|
||||
.studio-button.studio-button-primary:hover {
|
||||
-fx-background-color: #336698;
|
||||
-fx-border-color: #8cc6f7;
|
||||
}
|
||||
|
||||
.studio-button.studio-button-secondary,
|
||||
.studio-button.studio-button-cancel {
|
||||
-fx-background-color: #1b2430;
|
||||
-fx-border-color: #314154;
|
||||
-fx-text-fill: #d6e0ea;
|
||||
}
|
||||
|
||||
.studio-button.studio-button-secondary:hover,
|
||||
.studio-button.studio-button-cancel:hover {
|
||||
-fx-background-color: #243243;
|
||||
-fx-border-color: #556f8b;
|
||||
-fx-text-fill: #eef6ff;
|
||||
}
|
||||
|
||||
.studio-button.studio-button-warning {
|
||||
-fx-background-color: #4a3816;
|
||||
-fx-border-color: #c79a3d;
|
||||
-fx-text-fill: #ffe4a8;
|
||||
}
|
||||
|
||||
.studio-button.studio-button-warning:hover {
|
||||
-fx-background-color: #5b461b;
|
||||
-fx-border-color: #efbc59;
|
||||
}
|
||||
|
||||
.studio-button.studio-button-danger {
|
||||
-fx-background-color: #3b191c;
|
||||
-fx-border-color: #c7606a;
|
||||
-fx-text-fill: #ffd6d9;
|
||||
}
|
||||
|
||||
.studio-button.studio-button-danger:hover {
|
||||
-fx-background-color: #4b2025;
|
||||
-fx-border-color: #f08791;
|
||||
}
|
||||
|
||||
.studio-button.studio-button-ghost {
|
||||
-fx-background-color: transparent;
|
||||
-fx-border-color: transparent;
|
||||
-fx-text-fill: #d4d4d4;
|
||||
}
|
||||
|
||||
.studio-button.studio-button-ghost:hover {
|
||||
-fx-background-color: #2a2d2e;
|
||||
-fx-border-color: #3a414b;
|
||||
-fx-text-fill: #f2f7fb;
|
||||
}
|
||||
|
||||
.studio-button.studio-button-toggle:selected,
|
||||
.studio-button.studio-button-active {
|
||||
-fx-background-color: #24415e;
|
||||
-fx-border-color: #4d88bc;
|
||||
-fx-text-fill: #ffffff;
|
||||
}
|
||||
|
||||
.studio-button.studio-button-toggle:selected:hover,
|
||||
.studio-button.studio-button-active:hover {
|
||||
-fx-background-color: #2c5174;
|
||||
-fx-border-color: #79b1e1;
|
||||
}
|
||||
|
||||
.studio-button.studio-button-ghost.studio-button-toggle:selected {
|
||||
-fx-background-color: #37373d;
|
||||
-fx-border-color: transparent;
|
||||
-fx-text-fill: #f7fbff;
|
||||
}
|
||||
|
||||
.studio-button.studio-button-ghost.studio-button-toggle:selected:hover {
|
||||
-fx-background-color: #404047;
|
||||
}
|
||||
|
||||
.studio-workspace-rail-button {
|
||||
-fx-font-size: 16px;
|
||||
}
|
||||
|
||||
.studio-utility-placeholder {
|
||||
-fx-text-fill: #d4d4d4;
|
||||
-fx-padding: 16;
|
||||
@ -190,6 +282,12 @@
|
||||
-fx-accent: #5cb6ff;
|
||||
}
|
||||
|
||||
.assets-workspace-action-bar {
|
||||
-fx-alignment: center-left;
|
||||
-fx-spacing: 8;
|
||||
-fx-padding: 0 0 0 8;
|
||||
}
|
||||
|
||||
.assets-workspace-pane-title {
|
||||
-fx-text-fill: #f3f7fb;
|
||||
-fx-font-size: 15px;
|
||||
@ -209,21 +307,6 @@
|
||||
-fx-padding: 4 0 2 0;
|
||||
}
|
||||
|
||||
.assets-workspace-filter-button {
|
||||
-fx-background-color: #12161d;
|
||||
-fx-text-fill: #c7d5e5;
|
||||
-fx-background-radius: 999;
|
||||
-fx-border-radius: 999;
|
||||
-fx-border-color: #2d3948;
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
|
||||
.assets-workspace-filter-button:selected {
|
||||
-fx-background-color: #24415e;
|
||||
-fx-border-color: #4d88bc;
|
||||
-fx-text-fill: #ffffff;
|
||||
}
|
||||
|
||||
.assets-workspace-summary {
|
||||
-fx-text-fill: #9ecbff;
|
||||
-fx-font-size: 12px;
|
||||
@ -275,21 +358,91 @@
|
||||
-fx-border-color: #4f8dc3;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-icon {
|
||||
-fx-font-size: 14px;
|
||||
.assets-workspace-asset-row-selected:hover {
|
||||
-fx-background-color: #1d2c3c;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-row-tone-image {
|
||||
-fx-background-color: #121a23;
|
||||
-fx-border-color: #356187;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-row-tone-image:hover {
|
||||
-fx-background-color: #172433;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-row-tone-audio {
|
||||
-fx-background-color: #17131f;
|
||||
-fx-border-color: #70529b;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-row-tone-audio:hover {
|
||||
-fx-background-color: #21192c;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-row-tone-palette {
|
||||
-fx-background-color: #1d1711;
|
||||
-fx-border-color: #9c6d2b;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-row-tone-palette:hover {
|
||||
-fx-background-color: #281f15;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-row-tone-text {
|
||||
-fx-background-color: #122019;
|
||||
-fx-border-color: #3e8763;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-row-tone-text:hover {
|
||||
-fx-background-color: #162a20;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-row-tone-generic {
|
||||
-fx-background-color: #11151b;
|
||||
-fx-border-color: #334050;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-row-tone-generic:hover {
|
||||
-fx-background-color: #17202a;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-name {
|
||||
-fx-text-fill: #f6fbff;
|
||||
-fx-font-size: 13px;
|
||||
-fx-font-size: 15px;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-name-tone-image {
|
||||
-fx-text-fill: #9cd9ff;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-name-tone-audio {
|
||||
-fx-text-fill: #dcbfff;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-name-tone-palette {
|
||||
-fx-text-fill: #ffd69a;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-name-tone-text {
|
||||
-fx-text-fill: #aee8c5;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-name-tone-generic {
|
||||
-fx-text-fill: #f6fbff;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-path {
|
||||
-fx-text-fill: #9eacbb;
|
||||
-fx-font-size: 11px;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-badges {
|
||||
-fx-alignment: center-right;
|
||||
-fx-padding: 0 4 0 10;
|
||||
}
|
||||
|
||||
.assets-workspace-badge {
|
||||
-fx-font-size: 10px;
|
||||
-fx-padding: 3 7 3 7;
|
||||
@ -298,24 +451,12 @@
|
||||
-fx-border-width: 1;
|
||||
}
|
||||
|
||||
.assets-workspace-badge-managed {
|
||||
-fx-background-color: #153425;
|
||||
-fx-border-color: #2f8f59;
|
||||
-fx-text-fill: #8ce3ae;
|
||||
}
|
||||
|
||||
.assets-workspace-badge-orphan {
|
||||
-fx-background-color: #3a2d14;
|
||||
-fx-border-color: #bc8a31;
|
||||
-fx-text-fill: #ffd27a;
|
||||
}
|
||||
|
||||
.assets-workspace-badge-family {
|
||||
-fx-background-color: #1c2733;
|
||||
-fx-border-color: #38506a;
|
||||
-fx-text-fill: #b7d8f8;
|
||||
}
|
||||
|
||||
.assets-workspace-badge-preload {
|
||||
-fx-background-color: #271747;
|
||||
-fx-border-color: #7f65cf;
|
||||
@ -348,6 +489,20 @@
|
||||
-fx-padding: 4 0 4 0;
|
||||
}
|
||||
|
||||
.assets-details-summary-actions-row {
|
||||
-fx-spacing: 12;
|
||||
-fx-alignment: top-left;
|
||||
}
|
||||
|
||||
.assets-details-summary-actions-row > .assets-details-section:first-child {
|
||||
-fx-min-width: 0;
|
||||
}
|
||||
|
||||
.assets-details-summary-actions-row > .assets-details-section:last-child {
|
||||
-fx-pref-width: 280;
|
||||
-fx-max-width: 320;
|
||||
}
|
||||
|
||||
.assets-details-section {
|
||||
-fx-background-color: #11151b;
|
||||
-fx-background-radius: 12;
|
||||
@ -385,6 +540,27 @@
|
||||
-fx-font-size: 12px;
|
||||
}
|
||||
|
||||
.assets-details-readonly-check {
|
||||
-fx-text-fill: #eef4fb;
|
||||
-fx-font-size: 12px;
|
||||
}
|
||||
|
||||
.assets-details-readonly-check > .box {
|
||||
-fx-background-color: #0f1318;
|
||||
-fx-border-color: #3b4957;
|
||||
-fx-border-radius: 4;
|
||||
-fx-background-radius: 4;
|
||||
}
|
||||
|
||||
.assets-details-readonly-check:selected > .box {
|
||||
-fx-background-color: #17314f;
|
||||
-fx-border-color: #5cb6ff;
|
||||
}
|
||||
|
||||
.assets-details-readonly-check:selected > .box > .mark {
|
||||
-fx-background-color: #eef7ff;
|
||||
}
|
||||
|
||||
.assets-details-role-label {
|
||||
-fx-text-fill: #9fc3e7;
|
||||
-fx-font-size: 11px;
|
||||
@ -392,19 +568,9 @@
|
||||
}
|
||||
|
||||
.assets-details-input-button {
|
||||
-fx-background-color: #17202a;
|
||||
-fx-text-fill: #e6eff8;
|
||||
-fx-background-radius: 8;
|
||||
-fx-border-radius: 8;
|
||||
-fx-border-color: #2f4053;
|
||||
-fx-alignment: center-left;
|
||||
}
|
||||
|
||||
.assets-details-input-button-selected {
|
||||
-fx-background-color: #224160;
|
||||
-fx-border-color: #4f8dc3;
|
||||
}
|
||||
|
||||
.assets-details-input-preview-split {
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
@ -432,19 +598,6 @@
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.assets-details-preview-zoom-button {
|
||||
-fx-background-color: #17202a;
|
||||
-fx-text-fill: #e6eff8;
|
||||
-fx-background-radius: 999;
|
||||
-fx-border-radius: 999;
|
||||
-fx-border-color: #2f4053;
|
||||
}
|
||||
|
||||
.assets-details-preview-zoom-button:selected {
|
||||
-fx-background-color: #224160;
|
||||
-fx-border-color: #4f8dc3;
|
||||
}
|
||||
|
||||
.assets-details-preview-text {
|
||||
-fx-control-inner-background: #10161d;
|
||||
-fx-text-fill: #e4edf6;
|
||||
@ -484,25 +637,6 @@
|
||||
-fx-font-size: 12px;
|
||||
}
|
||||
|
||||
.assets-details-action-button {
|
||||
-fx-background-radius: 10;
|
||||
-fx-border-radius: 10;
|
||||
-fx-border-width: 1;
|
||||
-fx-padding: 8 12 8 12;
|
||||
}
|
||||
|
||||
.assets-details-action-button-primary {
|
||||
-fx-background-color: #173322;
|
||||
-fx-border-color: #2f8f59;
|
||||
-fx-text-fill: #c7f8d7;
|
||||
}
|
||||
|
||||
.assets-details-action-button-sensitive {
|
||||
-fx-background-color: #3b191c;
|
||||
-fx-border-color: #c7606a;
|
||||
-fx-text-fill: #ffd6d9;
|
||||
}
|
||||
|
||||
.assets-mutation-panel {
|
||||
-fx-background-color: #0f1318;
|
||||
-fx-background-radius: 12;
|
||||
@ -543,22 +677,6 @@
|
||||
-fx-font-size: 12px;
|
||||
}
|
||||
|
||||
.assets-mutation-cancel {
|
||||
-fx-background-color: #1b2430;
|
||||
-fx-text-fill: #d6e0ea;
|
||||
-fx-border-color: #314154;
|
||||
-fx-border-radius: 10;
|
||||
-fx-background-radius: 10;
|
||||
}
|
||||
|
||||
.assets-mutation-apply {
|
||||
-fx-background-color: #27507a;
|
||||
-fx-text-fill: #f7fbff;
|
||||
-fx-border-color: #5ea0de;
|
||||
-fx-border-radius: 10;
|
||||
-fx-background-radius: 10;
|
||||
}
|
||||
|
||||
.assets-workspace-logs-pane {
|
||||
-fx-collapsible: true;
|
||||
}
|
||||
|
||||
@ -54,12 +54,12 @@ final class StudioActivityEventMapperTest {
|
||||
@Test
|
||||
void mapsMutationPreviewReadyToInfoEntry() {
|
||||
final StudioActivityEntry entry = StudioActivityEventMapper
|
||||
.map(new StudioAssetsMutationPreviewReadyEvent(project(), AssetWorkspaceAction.QUARANTINE, 1))
|
||||
.map(new StudioAssetsMutationPreviewReadyEvent(project(), AssetWorkspaceAction.RELOCATE, 1))
|
||||
.orElseThrow();
|
||||
|
||||
assertEquals("Assets", entry.source());
|
||||
assertEquals(StudioActivityEntrySeverity.INFO, entry.severity());
|
||||
assertEquals("Preview ready: quarantine", entry.message());
|
||||
assertEquals("Preview ready: relocate", entry.message());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
final class AssetCreationCatalogTest {
|
||||
@Test
|
||||
void outputFormatsDependOnAssetType() {
|
||||
assertEquals(List.of("TILES/indexed_v1", "SPRITES/indexed_v1"), AssetCreationCatalog.outputFormatsFor("image_bank"));
|
||||
assertEquals(List.of("AUDIO/pcm_v1"), AssetCreationCatalog.outputFormatsFor("sound_bank"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void outputCodecsDependOnFormat() {
|
||||
assertEquals(List.of("RAW"), AssetCreationCatalog.outputCodecsFor("image_bank", "TILES/indexed_v1"));
|
||||
assertTrue(AssetCreationCatalog.supports("palette_bank", "PALETTE/indexed_v1", "RAW"));
|
||||
}
|
||||
}
|
||||
@ -15,8 +15,8 @@ final class AssetNavigatorProjectionBuilderTest {
|
||||
final Path assetsRoot = projectRoot.resolve("assets");
|
||||
final AssetNavigatorProjection projection = AssetNavigatorProjectionBuilder.build(
|
||||
List.of(
|
||||
managedAsset(1, "ui_atlas", "image_bank", assetsRoot.resolve("ui/atlas"), true, false),
|
||||
orphanAsset("menu_sounds", "sound_bank", assetsRoot.resolve("audio/menu"), false, false)),
|
||||
registeredAsset(1, "ui_atlas", "image_bank", assetsRoot.resolve("ui/atlas"), true, false),
|
||||
unregisteredAsset("menu_sounds", "sound_bank", assetsRoot.resolve("audio/menu"), false, false)),
|
||||
projectRoot,
|
||||
"",
|
||||
EnumSet.noneOf(AssetNavigatorFilter.class));
|
||||
@ -26,16 +26,16 @@ final class AssetNavigatorProjectionBuilderTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void managedAndOrphanFiltersBehaveAsStateFilterSet() {
|
||||
void registeredAndUnregisteredFiltersBehaveAsStateFilterSet() {
|
||||
final Path projectRoot = Path.of("/tmp/project");
|
||||
final Path assetsRoot = projectRoot.resolve("assets");
|
||||
final AssetNavigatorProjection projection = AssetNavigatorProjectionBuilder.build(
|
||||
List.of(
|
||||
managedAsset(1, "ui_atlas", "image_bank", assetsRoot.resolve("ui/atlas"), true, false),
|
||||
orphanAsset("menu_sounds", "sound_bank", assetsRoot.resolve("audio/menu"), false, false)),
|
||||
registeredAsset(1, "ui_atlas", "image_bank", assetsRoot.resolve("ui/atlas"), true, false),
|
||||
unregisteredAsset("menu_sounds", "sound_bank", assetsRoot.resolve("audio/menu"), false, false)),
|
||||
projectRoot,
|
||||
"",
|
||||
EnumSet.of(AssetNavigatorFilter.MANAGED));
|
||||
EnumSet.of(AssetNavigatorFilter.REGISTERED));
|
||||
|
||||
assertEquals(1, projection.visibleAssetCount());
|
||||
assertEquals("ui_atlas", projection.groups().getFirst().assets().getFirst().assetName());
|
||||
@ -47,12 +47,12 @@ final class AssetNavigatorProjectionBuilderTest {
|
||||
final Path assetsRoot = projectRoot.resolve("assets");
|
||||
final AssetNavigatorProjection projection = AssetNavigatorProjectionBuilder.build(
|
||||
List.of(
|
||||
managedAsset(1, "ui_atlas", "image_bank", assetsRoot.resolve("ui/atlas"), true, true),
|
||||
managedAsset(2, "bg_tiles", "image_bank", assetsRoot.resolve("bg/tiles"), true, false),
|
||||
managedAsset(3, "voice_bank", "sound_bank", assetsRoot.resolve("audio/voice"), false, true)),
|
||||
registeredAsset(1, "ui_atlas", "image_bank", assetsRoot.resolve("ui/atlas"), true, true),
|
||||
registeredAsset(2, "bg_tiles", "image_bank", assetsRoot.resolve("bg/tiles"), true, false),
|
||||
registeredAsset(3, "voice_bank", "sound_bank", assetsRoot.resolve("audio/voice"), false, true)),
|
||||
projectRoot,
|
||||
"",
|
||||
EnumSet.of(AssetNavigatorFilter.MANAGED, AssetNavigatorFilter.PRELOAD, AssetNavigatorFilter.DIAGNOSTICS));
|
||||
EnumSet.of(AssetNavigatorFilter.REGISTERED, AssetNavigatorFilter.PRELOAD, AssetNavigatorFilter.DIAGNOSTICS));
|
||||
|
||||
assertEquals(1, projection.visibleAssetCount());
|
||||
assertEquals("ui_atlas", projection.groups().getFirst().assets().getFirst().assetName());
|
||||
@ -63,8 +63,8 @@ final class AssetNavigatorProjectionBuilderTest {
|
||||
final Path projectRoot = Path.of("/tmp/project");
|
||||
final Path assetsRoot = projectRoot.resolve("assets");
|
||||
final List<AssetWorkspaceAssetSummary> assets = List.of(
|
||||
managedAsset(1, "ui_atlas", "image_bank", assetsRoot.resolve("ui/atlas"), true, false),
|
||||
orphanAsset("menu_sounds", "sound_bank", assetsRoot.resolve("audio/menu"), false, false));
|
||||
registeredAsset(1, "ui_atlas", "image_bank", assetsRoot.resolve("ui/atlas"), true, false),
|
||||
unregisteredAsset("menu_sounds", "sound_bank", assetsRoot.resolve("audio/menu"), false, false));
|
||||
|
||||
final AssetNavigatorProjection byName = AssetNavigatorProjectionBuilder.build(
|
||||
assets,
|
||||
@ -83,7 +83,7 @@ final class AssetNavigatorProjectionBuilderTest {
|
||||
assertEquals("menu_sounds", byPath.groups().getFirst().assets().getFirst().assetName());
|
||||
}
|
||||
|
||||
private AssetWorkspaceAssetSummary managedAsset(
|
||||
private AssetWorkspaceAssetSummary registeredAsset(
|
||||
int assetId,
|
||||
String name,
|
||||
String family,
|
||||
@ -93,7 +93,8 @@ final class AssetNavigatorProjectionBuilderTest {
|
||||
return new AssetWorkspaceAssetSummary(
|
||||
new AssetWorkspaceSelectionKey.ManagedAsset(assetId),
|
||||
name,
|
||||
AssetWorkspaceAssetState.MANAGED,
|
||||
AssetWorkspaceAssetState.REGISTERED,
|
||||
AssetWorkspaceBuildParticipation.INCLUDED,
|
||||
assetId,
|
||||
family,
|
||||
root,
|
||||
@ -101,7 +102,7 @@ final class AssetNavigatorProjectionBuilderTest {
|
||||
hasDiagnostics);
|
||||
}
|
||||
|
||||
private AssetWorkspaceAssetSummary orphanAsset(
|
||||
private AssetWorkspaceAssetSummary unregisteredAsset(
|
||||
String name,
|
||||
String family,
|
||||
Path root,
|
||||
@ -110,7 +111,8 @@ final class AssetNavigatorProjectionBuilderTest {
|
||||
return new AssetWorkspaceAssetSummary(
|
||||
new AssetWorkspaceSelectionKey.OrphanAsset(root),
|
||||
name,
|
||||
AssetWorkspaceAssetState.ORPHAN,
|
||||
AssetWorkspaceAssetState.UNREGISTERED,
|
||||
AssetWorkspaceBuildParticipation.EXCLUDED,
|
||||
null,
|
||||
family,
|
||||
root,
|
||||
|
||||
@ -9,40 +9,58 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
final class AssetWorkspaceActionSetBuilderTest {
|
||||
@Test
|
||||
void managedAssetsExposeDoctorBuildAndSensitiveMutations() {
|
||||
void includedRegisteredAssetsExposeOnlySensitiveAssetMutations() {
|
||||
final AssetWorkspaceActionSet actionSet = AssetWorkspaceActionSetBuilder.forAsset(new AssetWorkspaceAssetSummary(
|
||||
new AssetWorkspaceSelectionKey.ManagedAsset(42),
|
||||
"ui_atlas",
|
||||
AssetWorkspaceAssetState.MANAGED,
|
||||
AssetWorkspaceAssetState.REGISTERED,
|
||||
AssetWorkspaceBuildParticipation.INCLUDED,
|
||||
42,
|
||||
"image_bank",
|
||||
Path.of("/tmp/assets/ui_atlas"),
|
||||
true,
|
||||
false));
|
||||
|
||||
assertEquals(List.of(AssetWorkspaceAction.DOCTOR, AssetWorkspaceAction.BUILD), actionSet.primaryActions());
|
||||
assertEquals(List.of(), actionSet.primaryActions());
|
||||
assertEquals(
|
||||
List.of(
|
||||
AssetWorkspaceAction.FORGET,
|
||||
AssetWorkspaceAction.QUARANTINE,
|
||||
AssetWorkspaceAction.EXCLUDE_FROM_BUILD,
|
||||
AssetWorkspaceAction.RELOCATE,
|
||||
AssetWorkspaceAction.REMOVE),
|
||||
actionSet.sensitiveActions());
|
||||
}
|
||||
|
||||
@Test
|
||||
void orphanAssetsExposeAdoptRegisterAndSensitiveRelocationFlows() {
|
||||
void excludedRegisteredAssetsExposeIncludeInBuildAndSensitiveMutations() {
|
||||
final AssetWorkspaceActionSet actionSet = AssetWorkspaceActionSetBuilder.forAsset(new AssetWorkspaceAssetSummary(
|
||||
new AssetWorkspaceSelectionKey.ManagedAsset(42),
|
||||
"ui_atlas",
|
||||
AssetWorkspaceAssetState.REGISTERED,
|
||||
AssetWorkspaceBuildParticipation.EXCLUDED,
|
||||
42,
|
||||
"image_bank",
|
||||
Path.of("/tmp/assets/ui_atlas"),
|
||||
true,
|
||||
false));
|
||||
|
||||
assertEquals(List.of(AssetWorkspaceAction.INCLUDE_IN_BUILD), actionSet.primaryActions());
|
||||
assertEquals(List.of(AssetWorkspaceAction.RELOCATE, AssetWorkspaceAction.REMOVE), actionSet.sensitiveActions());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unregisteredAssetsExposeRegisterAndSensitiveRelocationFlows() {
|
||||
final AssetWorkspaceActionSet actionSet = AssetWorkspaceActionSetBuilder.forAsset(new AssetWorkspaceAssetSummary(
|
||||
new AssetWorkspaceSelectionKey.OrphanAsset(Path.of("/tmp/assets/ui_sounds")),
|
||||
"ui_sounds",
|
||||
AssetWorkspaceAssetState.ORPHAN,
|
||||
AssetWorkspaceAssetState.UNREGISTERED,
|
||||
AssetWorkspaceBuildParticipation.EXCLUDED,
|
||||
null,
|
||||
"sound_bank",
|
||||
Path.of("/tmp/assets/ui_sounds"),
|
||||
false,
|
||||
false));
|
||||
|
||||
assertEquals(List.of(AssetWorkspaceAction.ADOPT, AssetWorkspaceAction.REGISTER), actionSet.primaryActions());
|
||||
assertEquals(List.of(AssetWorkspaceAction.QUARANTINE, AssetWorkspaceAction.RELOCATE), actionSet.sensitiveActions());
|
||||
assertEquals(List.of(AssetWorkspaceAction.REGISTER), actionSet.primaryActions());
|
||||
assertEquals(List.of(AssetWorkspaceAction.RELOCATE), actionSet.sensitiveActions());
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,8 @@ final class AssetWorkspaceMutationImpactViewModelTest {
|
||||
final AssetWorkspaceAssetSummary asset = new AssetWorkspaceAssetSummary(
|
||||
new AssetWorkspaceSelectionKey.ManagedAsset(1),
|
||||
"ui_atlas",
|
||||
AssetWorkspaceAssetState.MANAGED,
|
||||
AssetWorkspaceAssetState.REGISTERED,
|
||||
AssetWorkspaceBuildParticipation.INCLUDED,
|
||||
1,
|
||||
"image_bank",
|
||||
Path.of("/tmp/assets/ui/atlas"),
|
||||
|
||||
@ -9,7 +9,7 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
final class AssetWorkspaceStateTest {
|
||||
@Test
|
||||
void preservesManagedSelectionByAssetIdAcrossRefresh() {
|
||||
void preservesRegisteredSelectionByAssetIdAcrossRefresh() {
|
||||
final AssetWorkspaceSelectionKey.ManagedAsset selected = new AssetWorkspaceSelectionKey.ManagedAsset(42);
|
||||
final List<AssetWorkspaceAssetSummary> assets = List.of(
|
||||
managedAsset(42, "ui_atlas", Path.of("/tmp/assets/ui_atlas")),
|
||||
@ -24,17 +24,17 @@ final class AssetWorkspaceStateTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void preservesOrphanSelectionByAssetRootAcrossRefresh() {
|
||||
final Path orphanRoot = Path.of("/tmp/assets/orphan_bank");
|
||||
final AssetWorkspaceSelectionKey.OrphanAsset selected = new AssetWorkspaceSelectionKey.OrphanAsset(orphanRoot);
|
||||
void preservesUnregisteredSelectionByAssetRootAcrossRefresh() {
|
||||
final Path unregisteredRoot = Path.of("/tmp/assets/orphan_bank");
|
||||
final AssetWorkspaceSelectionKey.OrphanAsset selected = new AssetWorkspaceSelectionKey.OrphanAsset(unregisteredRoot);
|
||||
final List<AssetWorkspaceAssetSummary> assets = List.of(
|
||||
orphanAsset("orphan_bank", orphanRoot),
|
||||
orphanAsset("other_bank", Path.of("/tmp/assets/other_bank")));
|
||||
unregisteredAsset("orphan_bank", unregisteredRoot),
|
||||
unregisteredAsset("other_bank", Path.of("/tmp/assets/other_bank")));
|
||||
|
||||
final AssetWorkspaceState state = AssetWorkspaceState.ready(assets, selected);
|
||||
|
||||
assertEquals(selected, state.selectedKey());
|
||||
assertEquals(orphanRoot.toAbsolutePath().normalize(), state.selectedAsset().orElseThrow().assetRoot());
|
||||
assertEquals(unregisteredRoot.toAbsolutePath().normalize(), state.selectedAsset().orElseThrow().assetRoot());
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -62,7 +62,8 @@ final class AssetWorkspaceStateTest {
|
||||
return new AssetWorkspaceAssetSummary(
|
||||
new AssetWorkspaceSelectionKey.ManagedAsset(assetId),
|
||||
name,
|
||||
AssetWorkspaceAssetState.MANAGED,
|
||||
AssetWorkspaceAssetState.REGISTERED,
|
||||
AssetWorkspaceBuildParticipation.INCLUDED,
|
||||
assetId,
|
||||
"image_bank",
|
||||
root,
|
||||
@ -70,11 +71,12 @@ final class AssetWorkspaceStateTest {
|
||||
false);
|
||||
}
|
||||
|
||||
private AssetWorkspaceAssetSummary orphanAsset(String name, Path root) {
|
||||
private AssetWorkspaceAssetSummary unregisteredAsset(String name, Path root) {
|
||||
return new AssetWorkspaceAssetSummary(
|
||||
new AssetWorkspaceSelectionKey.OrphanAsset(root),
|
||||
name,
|
||||
AssetWorkspaceAssetState.ORPHAN,
|
||||
AssetWorkspaceAssetState.UNREGISTERED,
|
||||
AssetWorkspaceBuildParticipation.EXCLUDED,
|
||||
null,
|
||||
"image_bank",
|
||||
root,
|
||||
|
||||
@ -14,51 +14,42 @@ final class FileSystemAssetWorkspaceMutationServiceTest {
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void previewQuarantineForManagedAssetShowsRegistryAndWorkspaceImpact() throws Exception {
|
||||
void previewRelocateForManagedAssetIsHighRiskAndPreservesIdentityContract() throws Exception {
|
||||
final Path projectRoot = createManagedAssetProject();
|
||||
final FileSystemAssetWorkspaceMutationService service = new FileSystemAssetWorkspaceMutationService();
|
||||
final AssetWorkspaceAssetSummary asset = managedAsset(projectRoot);
|
||||
final Path customTarget = projectRoot.resolve("assets/ui/atlas-v2");
|
||||
|
||||
final AssetWorkspaceMutationPreview preview = service.preview(project("Main", projectRoot), asset, AssetWorkspaceAction.QUARANTINE);
|
||||
final AssetWorkspaceMutationPreview preview = service.preview(
|
||||
project("Main", projectRoot),
|
||||
asset,
|
||||
AssetWorkspaceAction.RELOCATE,
|
||||
customTarget);
|
||||
|
||||
assertTrue(preview.canApply());
|
||||
assertFalse(preview.highRisk());
|
||||
assertNotNull(preview.targetAssetRoot());
|
||||
assertEquals(1, preview.changes().stream().filter(change -> change.scope() == AssetWorkspaceMutationChangeScope.REGISTRY).count());
|
||||
assertEquals(1, preview.changes().stream().filter(change -> change.scope() == AssetWorkspaceMutationChangeScope.WORKSPACE).count());
|
||||
assertTrue(preview.highRisk());
|
||||
assertEquals(customTarget.toAbsolutePath().normalize(), preview.targetAssetRoot());
|
||||
assertTrue(preview.changes().stream().anyMatch(change ->
|
||||
change.scope() == AssetWorkspaceMutationChangeScope.REGISTRY && change.verb().equals("UPDATE")));
|
||||
assertTrue(preview.changes().stream().anyMatch(change ->
|
||||
change.scope() == AssetWorkspaceMutationChangeScope.WORKSPACE && change.verb().equals("MOVE")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyQuarantineMovesAssetAndRemovesRegistryEntry() throws Exception {
|
||||
void applyExcludeFromBuildPreservesRegistrationAndUpdatesRegistryState() throws Exception {
|
||||
final Path projectRoot = createManagedAssetProject();
|
||||
final FileSystemAssetWorkspaceMutationService service = new FileSystemAssetWorkspaceMutationService();
|
||||
final AssetWorkspaceAssetSummary asset = managedAsset(projectRoot);
|
||||
final ProjectReference project = project("Main", projectRoot);
|
||||
|
||||
final AssetWorkspaceMutationPreview preview = service.preview(project, asset, AssetWorkspaceAction.QUARANTINE);
|
||||
final AssetWorkspaceMutationPreview preview = service.preview(project, asset, AssetWorkspaceAction.EXCLUDE_FROM_BUILD, null);
|
||||
assertTrue(preview.canApply());
|
||||
|
||||
service.apply(project, preview);
|
||||
|
||||
assertFalse(Files.exists(asset.assetRoot()));
|
||||
assertTrue(Files.isDirectory(preview.targetAssetRoot()));
|
||||
final String registryJson = Files.readString(projectRoot.resolve("assets/.prometeu/index.json"));
|
||||
assertFalse(registryJson.contains("\"root\" : \"ui/atlas\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void previewRelocateForManagedAssetIsHighRiskAndPreservesIdentityContract() throws Exception {
|
||||
final Path projectRoot = createManagedAssetProject();
|
||||
final FileSystemAssetWorkspaceMutationService service = new FileSystemAssetWorkspaceMutationService();
|
||||
final AssetWorkspaceAssetSummary asset = managedAsset(projectRoot);
|
||||
|
||||
final AssetWorkspaceMutationPreview preview = service.preview(project("Main", projectRoot), asset, AssetWorkspaceAction.RELOCATE);
|
||||
|
||||
assertTrue(preview.canApply());
|
||||
assertTrue(preview.highRisk());
|
||||
assertNotNull(preview.targetAssetRoot());
|
||||
assertTrue(preview.changes().stream().anyMatch(change ->
|
||||
change.scope() == AssetWorkspaceMutationChangeScope.REGISTRY && change.verb().equals("UPDATE")));
|
||||
assertTrue(preview.changes().stream().anyMatch(change ->
|
||||
change.scope() == AssetWorkspaceMutationChangeScope.WORKSPACE && change.verb().equals("MOVE")));
|
||||
assertTrue(registryJson.contains("\"asset_id\" : 1"));
|
||||
assertTrue(registryJson.contains("\"included_in_build\" : false"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -67,15 +58,33 @@ final class FileSystemAssetWorkspaceMutationServiceTest {
|
||||
final FileSystemAssetWorkspaceMutationService service = new FileSystemAssetWorkspaceMutationService();
|
||||
final AssetWorkspaceAssetSummary asset = managedAsset(projectRoot);
|
||||
final ProjectReference project = project("Main", projectRoot);
|
||||
final Path customTarget = projectRoot.resolve("assets/reorganized/atlas");
|
||||
|
||||
final AssetWorkspaceMutationPreview preview = service.preview(project, asset, AssetWorkspaceAction.RELOCATE);
|
||||
final AssetWorkspaceMutationPreview preview = service.preview(project, asset, AssetWorkspaceAction.RELOCATE, customTarget);
|
||||
service.apply(project, preview);
|
||||
|
||||
assertFalse(Files.exists(asset.assetRoot()));
|
||||
assertTrue(Files.isDirectory(preview.targetAssetRoot()));
|
||||
assertTrue(Files.isDirectory(customTarget));
|
||||
final String registryJson = Files.readString(projectRoot.resolve("assets/.prometeu/index.json"));
|
||||
assertTrue(registryJson.contains("\"asset_id\" : 1"));
|
||||
assertTrue(registryJson.contains(preview.targetAssetRoot().getFileName().toString()));
|
||||
assertTrue(registryJson.contains("reorganized/atlas"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void previewRelocateWithExistingDestinationCreatesBlocker() throws Exception {
|
||||
final Path projectRoot = createManagedAssetProject();
|
||||
final Path existingTarget = projectRoot.resolve("assets/ui/existing");
|
||||
Files.createDirectories(existingTarget);
|
||||
final FileSystemAssetWorkspaceMutationService service = new FileSystemAssetWorkspaceMutationService();
|
||||
|
||||
final AssetWorkspaceMutationPreview preview = service.preview(
|
||||
project("Main", projectRoot),
|
||||
managedAsset(projectRoot),
|
||||
AssetWorkspaceAction.RELOCATE,
|
||||
existingTarget);
|
||||
|
||||
assertFalse(preview.canApply());
|
||||
assertTrue(preview.blockers().stream().anyMatch(message -> message.contains("already exists")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -85,14 +94,15 @@ final class FileSystemAssetWorkspaceMutationServiceTest {
|
||||
final AssetWorkspaceAssetSummary asset = new AssetWorkspaceAssetSummary(
|
||||
new AssetWorkspaceSelectionKey.OrphanAsset(projectRoot.resolve("assets/missing")),
|
||||
"missing_asset",
|
||||
AssetWorkspaceAssetState.ORPHAN,
|
||||
AssetWorkspaceAssetState.UNREGISTERED,
|
||||
AssetWorkspaceBuildParticipation.EXCLUDED,
|
||||
null,
|
||||
"unknown",
|
||||
projectRoot.resolve("assets/missing"),
|
||||
false,
|
||||
false);
|
||||
|
||||
final AssetWorkspaceMutationPreview preview = service.preview(project("Main", projectRoot), asset, AssetWorkspaceAction.QUARANTINE);
|
||||
final AssetWorkspaceMutationPreview preview = service.preview(project("Main", projectRoot), asset, AssetWorkspaceAction.RELOCATE);
|
||||
|
||||
assertFalse(preview.canApply());
|
||||
assertFalse(preview.blockers().isEmpty());
|
||||
@ -118,7 +128,8 @@ final class FileSystemAssetWorkspaceMutationServiceTest {
|
||||
{
|
||||
"asset_id": 1,
|
||||
"asset_uuid": "uuid-1",
|
||||
"root": "ui/atlas"
|
||||
"root": "ui/atlas",
|
||||
"included_in_build": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -130,7 +141,8 @@ final class FileSystemAssetWorkspaceMutationServiceTest {
|
||||
return new AssetWorkspaceAssetSummary(
|
||||
new AssetWorkspaceSelectionKey.ManagedAsset(1),
|
||||
"ui_atlas",
|
||||
AssetWorkspaceAssetState.MANAGED,
|
||||
AssetWorkspaceAssetState.REGISTERED,
|
||||
AssetWorkspaceBuildParticipation.INCLUDED,
|
||||
1,
|
||||
"image_bank",
|
||||
projectRoot.resolve("assets/ui/atlas"),
|
||||
|
||||
@ -25,7 +25,7 @@ final class FileSystemAssetWorkspaceServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void marksRegistryRootsAsManagedAndUnregisteredAnchorsAsOrphan() throws Exception {
|
||||
void marksRegistryRootsAsRegisteredAndUnregisteredAnchorsAsExcluded() throws Exception {
|
||||
final Path projectRoot = tempDir.resolve("main");
|
||||
final Path assetsRoot = projectRoot.resolve("assets");
|
||||
final Path managedRoot = assetsRoot.resolve("ui").resolve("atlas");
|
||||
@ -66,21 +66,23 @@ final class FileSystemAssetWorkspaceServiceTest {
|
||||
final AssetWorkspaceSnapshot snapshot = service.loadWorkspace(project("Main", projectRoot));
|
||||
|
||||
assertEquals(2, snapshot.assets().size());
|
||||
final AssetWorkspaceAssetSummary managed = snapshot.assets().stream()
|
||||
final AssetWorkspaceAssetSummary registered = snapshot.assets().stream()
|
||||
.filter(asset -> asset.assetName().equals("ui_atlas"))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
final AssetWorkspaceAssetSummary orphan = snapshot.assets().stream()
|
||||
final AssetWorkspaceAssetSummary unregistered = snapshot.assets().stream()
|
||||
.filter(asset -> asset.assetName().equals("ui_sounds"))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
|
||||
assertEquals(AssetWorkspaceAssetState.MANAGED, managed.state());
|
||||
assertEquals(1, managed.assetId());
|
||||
assertTrue(managed.preload());
|
||||
assertEquals(AssetWorkspaceAssetState.REGISTERED, registered.state());
|
||||
assertEquals(AssetWorkspaceBuildParticipation.INCLUDED, registered.buildParticipation());
|
||||
assertEquals(1, registered.assetId());
|
||||
assertTrue(registered.preload());
|
||||
|
||||
assertEquals(AssetWorkspaceAssetState.ORPHAN, orphan.state());
|
||||
assertEquals(null, orphan.assetId());
|
||||
assertEquals(AssetWorkspaceAssetState.UNREGISTERED, unregistered.state());
|
||||
assertEquals(AssetWorkspaceBuildParticipation.EXCLUDED, unregistered.buildParticipation());
|
||||
assertEquals(null, unregistered.assetId());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@ -0,0 +1,95 @@
|
||||
package p.studio.workspaces.assets;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import p.studio.projects.ProjectReference;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
final class PackerBackedAssetCreationServiceTest {
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void createWritesManifestAndRegistersManagedAsset() throws Exception {
|
||||
final Path projectRoot = tempDir.resolve("main");
|
||||
Files.createDirectories(projectRoot.resolve("assets"));
|
||||
final PackerBackedAssetCreationService service = new PackerBackedAssetCreationService();
|
||||
|
||||
final AssetCreationResult result = service.create(project(), new AssetCreationRequest(
|
||||
"ui_atlas",
|
||||
"ui/atlas",
|
||||
"image_bank",
|
||||
"TILES/indexed_v1",
|
||||
"RAW",
|
||||
true));
|
||||
|
||||
assertEquals(new AssetWorkspaceSelectionKey.ManagedAsset(1), result.selectionKey());
|
||||
assertTrue(Files.isDirectory(projectRoot.resolve("assets/ui/atlas")));
|
||||
final String manifest = Files.readString(projectRoot.resolve("assets/ui/atlas/asset.json"));
|
||||
assertTrue(manifest.contains("\"name\" : \"ui_atlas\""));
|
||||
assertTrue(manifest.contains("\"type\" : \"image_bank\""));
|
||||
assertTrue(manifest.contains("\"format\" : \"TILES/indexed_v1\""));
|
||||
assertTrue(manifest.contains("\"codec\" : \"RAW\""));
|
||||
assertTrue(manifest.contains("\"inputs\" : { }"));
|
||||
final String registry = Files.readString(projectRoot.resolve("assets/.prometeu/index.json"));
|
||||
assertTrue(registry.contains("\"asset_id\" : 1"));
|
||||
assertTrue(registry.contains("\"root\" : \"ui/atlas\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createRejectsUntrustedRelativeRoot() throws Exception {
|
||||
final Path projectRoot = tempDir.resolve("main");
|
||||
Files.createDirectories(projectRoot.resolve("assets"));
|
||||
final PackerBackedAssetCreationService service = new PackerBackedAssetCreationService();
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> service.create(project(), new AssetCreationRequest(
|
||||
"ui_atlas",
|
||||
"../escape",
|
||||
"image_bank",
|
||||
"TILES/indexed_v1",
|
||||
"RAW",
|
||||
true)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createRejectsExistingAssetManifest() throws Exception {
|
||||
final Path projectRoot = tempDir.resolve("main");
|
||||
final Path assetRoot = projectRoot.resolve("assets/ui/existing");
|
||||
Files.createDirectories(assetRoot);
|
||||
Files.writeString(assetRoot.resolve("asset.json"), "{}");
|
||||
final PackerBackedAssetCreationService service = new PackerBackedAssetCreationService();
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> service.create(project(), new AssetCreationRequest(
|
||||
"ui_existing",
|
||||
"ui/existing",
|
||||
"image_bank",
|
||||
"TILES/indexed_v1",
|
||||
"RAW",
|
||||
true)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createRejectsUnsupportedTypeFormatCodecCombination() throws Exception {
|
||||
final Path projectRoot = tempDir.resolve("main");
|
||||
Files.createDirectories(projectRoot.resolve("assets"));
|
||||
final PackerBackedAssetCreationService service = new PackerBackedAssetCreationService();
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> service.create(project(), new AssetCreationRequest(
|
||||
"ui_atlas",
|
||||
"ui/atlas",
|
||||
"sound_bank",
|
||||
"TILES/indexed_v1",
|
||||
"RAW",
|
||||
true)));
|
||||
}
|
||||
|
||||
private ProjectReference project() {
|
||||
return new ProjectReference("Main", "1.0.0", "pbs", 1, tempDir.resolve("main"));
|
||||
}
|
||||
}
|
||||
@ -37,7 +37,8 @@ final class PackerBackedAssetWorkspaceMutationServiceTest {
|
||||
final AssetWorkspaceMutationPreview preview = service.preview(
|
||||
project("Main", projectRoot),
|
||||
managedAsset(projectRoot),
|
||||
AssetWorkspaceAction.QUARANTINE);
|
||||
AssetWorkspaceAction.RELOCATE,
|
||||
projectRoot.resolve("assets/reorganized/atlas"));
|
||||
|
||||
assertEquals(1, previewEvents.size());
|
||||
assertTrue(preview.canApply());
|
||||
@ -53,16 +54,19 @@ final class PackerBackedAssetWorkspaceMutationServiceTest {
|
||||
final List<StudioAssetsMutationAppliedEvent> appliedEvents = new ArrayList<>();
|
||||
globalBus.subscribe(StudioAssetsMutationAppliedEvent.class, appliedEvents::add);
|
||||
final PackerBackedAssetWorkspaceMutationService service = service(globalBus);
|
||||
final Path customTarget = projectRoot.resolve("assets/reorganized/atlas");
|
||||
final AssetWorkspaceMutationPreview preview = service.preview(
|
||||
project("Main", projectRoot),
|
||||
managedAsset(projectRoot),
|
||||
AssetWorkspaceAction.RELOCATE);
|
||||
AssetWorkspaceAction.RELOCATE,
|
||||
customTarget);
|
||||
|
||||
service.apply(project("Main", projectRoot), preview);
|
||||
|
||||
assertEquals(1, appliedEvents.size());
|
||||
assertEquals(AssetWorkspaceAction.RELOCATE, appliedEvents.getFirst().action());
|
||||
assertTrue(Files.isDirectory(preview.targetAssetRoot()));
|
||||
assertEquals(customTarget.toAbsolutePath().normalize(), preview.targetAssetRoot());
|
||||
assertTrue(Files.isDirectory(customTarget));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -72,10 +76,12 @@ final class PackerBackedAssetWorkspaceMutationServiceTest {
|
||||
final List<StudioAssetsMutationFailedEvent> failedEvents = new ArrayList<>();
|
||||
globalBus.subscribe(StudioAssetsMutationFailedEvent.class, failedEvents::add);
|
||||
final PackerBackedAssetWorkspaceMutationService service = service(globalBus);
|
||||
final Path customTarget = projectRoot.resolve("assets/reorganized/atlas");
|
||||
final AssetWorkspaceMutationPreview preview = service.preview(
|
||||
project("Main", projectRoot),
|
||||
managedAsset(projectRoot),
|
||||
AssetWorkspaceAction.RELOCATE);
|
||||
AssetWorkspaceAction.RELOCATE,
|
||||
customTarget);
|
||||
|
||||
deleteRecursively(projectRoot.resolve("assets/ui/atlas"));
|
||||
|
||||
@ -85,6 +91,27 @@ final class PackerBackedAssetWorkspaceMutationServiceTest {
|
||||
assertFalse(failedEvents.getFirst().message().isBlank());
|
||||
}
|
||||
|
||||
@Test
|
||||
void previewRelocateWithExistingDestinationReturnsBlockerWithoutCallingPacker() throws Exception {
|
||||
final Path projectRoot = createManagedAssetProject();
|
||||
final Path existingTarget = projectRoot.resolve("assets/ui/existing");
|
||||
Files.createDirectories(existingTarget);
|
||||
final StudioEventBus globalBus = new StudioEventBus();
|
||||
final List<StudioAssetsMutationPreviewReadyEvent> previewEvents = new ArrayList<>();
|
||||
globalBus.subscribe(StudioAssetsMutationPreviewReadyEvent.class, previewEvents::add);
|
||||
final PackerBackedAssetWorkspaceMutationService service = service(globalBus);
|
||||
|
||||
final AssetWorkspaceMutationPreview preview = service.preview(
|
||||
project("Main", projectRoot),
|
||||
managedAsset(projectRoot),
|
||||
AssetWorkspaceAction.RELOCATE,
|
||||
existingTarget);
|
||||
|
||||
assertEquals(1, previewEvents.size());
|
||||
assertFalse(preview.canApply());
|
||||
assertTrue(preview.blockers().stream().anyMatch(message -> message.contains("already exists")));
|
||||
}
|
||||
|
||||
private PackerBackedAssetWorkspaceMutationService service(StudioEventBus globalBus) {
|
||||
return new PackerBackedAssetWorkspaceMutationService(
|
||||
new StudioWorkspaceEventBus(WorkspaceId.ASSETS, globalBus),
|
||||
@ -126,7 +153,8 @@ final class PackerBackedAssetWorkspaceMutationServiceTest {
|
||||
return new AssetWorkspaceAssetSummary(
|
||||
new AssetWorkspaceSelectionKey.ManagedAsset(1),
|
||||
"ui_atlas",
|
||||
AssetWorkspaceAssetState.MANAGED,
|
||||
AssetWorkspaceAssetState.REGISTERED,
|
||||
AssetWorkspaceBuildParticipation.INCLUDED,
|
||||
1,
|
||||
"image_bank",
|
||||
projectRoot.resolve("assets/ui/atlas"),
|
||||
|
||||
@ -60,8 +60,8 @@ final class PackerBackedAssetWorkspaceServiceTest {
|
||||
final AssetWorkspaceSnapshot snapshot = service.loadWorkspace(project("Main", projectRoot));
|
||||
|
||||
assertEquals(2, snapshot.assets().size());
|
||||
assertTrue(snapshot.assets().stream().anyMatch(asset -> asset.state() == AssetWorkspaceAssetState.MANAGED));
|
||||
assertTrue(snapshot.assets().stream().anyMatch(asset -> asset.state() == AssetWorkspaceAssetState.ORPHAN));
|
||||
assertTrue(snapshot.assets().stream().anyMatch(asset -> asset.state() == AssetWorkspaceAssetState.REGISTERED));
|
||||
assertTrue(snapshot.assets().stream().anyMatch(asset -> asset.state() == AssetWorkspaceAssetState.UNREGISTERED));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@ -69,7 +69,7 @@ final class PackerStudioIntegrationTest {
|
||||
assertEquals(2, snapshot.assets().size());
|
||||
|
||||
final AssetWorkspaceAssetSummary orphan = snapshot.assets().stream()
|
||||
.filter(asset -> asset.state() == AssetWorkspaceAssetState.ORPHAN)
|
||||
.filter(asset -> asset.state() == AssetWorkspaceAssetState.UNREGISTERED)
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
final AssetWorkspaceMutationPreview preview = mutationService.preview(project, orphan, AssetWorkspaceAction.REGISTER);
|
||||
|
||||
@ -1,9 +1,20 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"next_asset_id" : 2,
|
||||
"next_asset_id" : 9,
|
||||
"assets" : [ {
|
||||
"asset_id" : 1,
|
||||
"asset_uuid" : "67cd978d-cd61-4641-ba9e-98fe4bc039bd",
|
||||
"root" : "ui/atlas-relocated"
|
||||
"asset_id" : 3,
|
||||
"asset_uuid" : "21953cb8-4101-4790-9e5e-d95f5fbc9b5a",
|
||||
"root" : "ui/atlas2",
|
||||
"included_in_build" : true
|
||||
}, {
|
||||
"asset_id" : 7,
|
||||
"asset_uuid" : "62a81570-8f47-4612-9288-6060e6c9a2e2",
|
||||
"root" : "ui/one-more-atlas",
|
||||
"included_in_build" : true
|
||||
}, {
|
||||
"asset_id" : 8,
|
||||
"asset_uuid" : "9a7386e7-6f0e-4e4c-9919-0de71e0b7031",
|
||||
"root" : "sound",
|
||||
"included_in_build" : true
|
||||
} ]
|
||||
}
|
||||
13
test-projects/main/assets/sound/asset.json
Normal file
13
test-projects/main/assets/sound/asset.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"name" : "bla",
|
||||
"type" : "sound_bank",
|
||||
"inputs" : { },
|
||||
"output" : {
|
||||
"format" : "AUDIO/pcm_v1",
|
||||
"codec" : "RAW"
|
||||
},
|
||||
"preload" : {
|
||||
"enabled" : true
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 137 B After Width: | Height: | Size: 137 B |
13
test-projects/main/assets/ui/one-more-atlas/asset.json
Normal file
13
test-projects/main/assets/ui/one-more-atlas/asset.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"name" : "one-more-atlas",
|
||||
"type" : "image_bank",
|
||||
"inputs" : { },
|
||||
"output" : {
|
||||
"format" : "TILES/indexed_v1",
|
||||
"codec" : "RAW"
|
||||
},
|
||||
"preload" : {
|
||||
"enabled" : true
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user