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 --dir` creates a dedicated asset root dir
|
||||||
* `add --in-place` anchors next to the file
|
* `add --in-place` anchors next to the file
|
||||||
|
|
||||||
### 13.3 `prometeu packer adopt`
|
### 13.3 `prometeu packer forget <name|id|uuid>`
|
||||||
|
|
||||||
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>`
|
|
||||||
|
|
||||||
Removes an asset from the registry without deleting files.
|
Removes an asset from the registry without deleting files.
|
||||||
|
|
||||||
Useful for WIP and cleanup.
|
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.
|
Removes the asset from the registry.
|
||||||
|
|
||||||
* default: no deletion
|
* default: no deletion
|
||||||
* `--delete` can remove the asset root dir (dangerous; must confirm in UI tooling, or require a force flag in CLI)
|
* `--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:
|
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.
|
- each `asset.json` tells the packer what that asset declares locally.
|
||||||
|
|
||||||
This split matters.
|
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`?
|
## 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/`
|
- project workspace with `assets/`
|
||||||
- packer registry and control data under `assets/.prometeu/`
|
- packer registry and control data under `assets/.prometeu/`
|
||||||
- managed asset roots anchored by `asset.json`
|
- registered asset roots anchored by `asset.json`
|
||||||
|
|
||||||
## Core Rules
|
## Core Rules
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# Workspace, Registry, and Asset Identity Specification
|
# Workspace, Registry, and Asset Identity Specification
|
||||||
|
|
||||||
Status: Draft
|
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.
|
Purpose: Define how the packer recognizes, tracks, and preserves asset identity.
|
||||||
|
|
||||||
## Authority and Precedence
|
## Authority and Precedence
|
||||||
@ -16,18 +16,22 @@ This specification consolidates the initial packer agenda and decision wave into
|
|||||||
|
|
||||||
## Core Rules
|
## 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`.
|
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.
|
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
|
## Identity Model
|
||||||
|
|
||||||
Each managed asset has:
|
Each registered asset has:
|
||||||
|
|
||||||
- `asset_id`: stable project-local identity
|
- `asset_id`: stable project-local identity
|
||||||
- `asset_uuid`: stable long-lived identity for migration/tooling scenarios
|
- `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:
|
The following are not primary identity:
|
||||||
|
|
||||||
@ -50,15 +54,26 @@ Rules:
|
|||||||
|
|
||||||
Renaming `asset_name` is an API-visible change, but not an identity change.
|
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:
|
Rules:
|
||||||
|
|
||||||
- it does not enter the build automatically;
|
- it is excluded from the build automatically;
|
||||||
- it is diagnosable;
|
- 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
|
## Asset ID Allocation
|
||||||
|
|
||||||
@ -77,7 +92,7 @@ Identity-bearing conflicts are structural errors.
|
|||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
- duplicate or ambiguous anchors under managed expectations;
|
- duplicate or ambiguous anchors under registered expectations;
|
||||||
- manual copy that creates identity collision;
|
- manual copy that creates identity collision;
|
||||||
- registered root missing anchor.
|
- registered root missing anchor.
|
||||||
|
|
||||||
@ -91,6 +106,6 @@ Examples:
|
|||||||
|
|
||||||
This specification is complete enough when:
|
This specification is complete enough when:
|
||||||
|
|
||||||
- managed asset boundaries are unambiguous;
|
- registered asset boundaries are unambiguous;
|
||||||
- registry authority is explicit;
|
- registry authority is explicit;
|
||||||
- identity survives relocation without ambiguity.
|
- identity survives relocation without ambiguity.
|
||||||
|
|||||||
@ -92,7 +92,7 @@ Rules:
|
|||||||
|
|
||||||
## Preload
|
## Preload
|
||||||
|
|
||||||
Each managed asset must declare preload intent explicitly.
|
Each registered asset must declare preload intent explicitly.
|
||||||
|
|
||||||
Baseline shape:
|
Baseline shape:
|
||||||
|
|
||||||
|
|||||||
@ -74,7 +74,7 @@ The header carries:
|
|||||||
|
|
||||||
Rules:
|
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 secondary runtime-only identity layer;
|
||||||
- no synthetic dense reindexing layer;
|
- no synthetic dense reindexing layer;
|
||||||
- `asset_name` remains present as logical/API-facing metadata.
|
- `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:
|
Diagnostics are divided into:
|
||||||
|
|
||||||
- structural managed-world errors;
|
- structural registered-world errors;
|
||||||
- advisory workspace hygiene diagnostics.
|
- advisory workspace hygiene diagnostics.
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
- structural managed-world errors block builds;
|
- structural registered-world errors block builds;
|
||||||
- hygiene findings do not block baseline builds by default;
|
- hygiene findings do not block baseline builds by default;
|
||||||
- workspace scanning is broader than build validation.
|
- workspace scanning is broader than build validation.
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ Rules:
|
|||||||
|
|
||||||
Baseline doctor behavior:
|
Baseline doctor behavior:
|
||||||
|
|
||||||
- managed-world validation by default;
|
- registered-world validation by default;
|
||||||
- broader workspace hygiene scanning in expanded workspace mode;
|
- broader workspace hygiene scanning in expanded workspace mode;
|
||||||
- safe mechanical fix application only for baseline fix flows.
|
- safe mechanical fix application only for baseline fix flows.
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ Rules:
|
|||||||
- destructive or relocational mutations require explicit consent;
|
- destructive or relocational mutations require explicit consent;
|
||||||
- quarantine is explicit and reversible;
|
- quarantine is explicit and reversible;
|
||||||
- duplicate content detection is advisory, not structurally invalid by itself;
|
- 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
|
## Studio-First Service Surface
|
||||||
|
|
||||||
@ -48,8 +48,8 @@ Baseline core services:
|
|||||||
|
|
||||||
- `init_workspace`
|
- `init_workspace`
|
||||||
- `register_asset`
|
- `register_asset`
|
||||||
- `adopt_asset`
|
- `include_asset_in_build`
|
||||||
- `forget_asset`
|
- `exclude_asset_from_build`
|
||||||
- `remove_asset`
|
- `remove_asset`
|
||||||
- `list_assets`
|
- `list_assets`
|
||||||
- `get_asset_details`
|
- `get_asset_details`
|
||||||
@ -92,7 +92,7 @@ Rules:
|
|||||||
|
|
||||||
- preview or analysis precedes broad mutation where appropriate;
|
- preview or analysis precedes broad mutation where appropriate;
|
||||||
- safe-fix flows are modeled as services, not merely terminal flags;
|
- 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
|
## Structured Responses
|
||||||
|
|
||||||
|
|||||||
@ -45,8 +45,8 @@ The `Assets` workspace is a Studio view over packer services.
|
|||||||
|
|
||||||
The workspace must assume:
|
The workspace must assume:
|
||||||
|
|
||||||
- managed assets are the primary unit of identity and operation;
|
- registered and unregistered assets may coexist inside `assets/`;
|
||||||
- orphan declarations may exist and must remain visible;
|
- unregistered assets remain visible but excluded from builds;
|
||||||
- assets may aggregate many internal inputs;
|
- assets may aggregate many internal inputs;
|
||||||
- runtime-facing output contract data exists for each asset;
|
- runtime-facing output contract data exists for each asset;
|
||||||
- diagnostics, activity, progress, and staged mutation responses are available from Studio-facing services.
|
- 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:
|
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;
|
- where the asset root lives;
|
||||||
- which internal inputs belong to that asset;
|
- which internal inputs belong to that asset;
|
||||||
- what the asset declares toward the runtime-facing contract;
|
- 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`.
|
- 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 expose strong visual semantics through icons, badges, and state styling.
|
||||||
- Asset nodes must surface:
|
- Asset nodes must surface:
|
||||||
- managed/orphan state,
|
- registration status,
|
||||||
|
- build participation,
|
||||||
- diagnostics presence,
|
- diagnostics presence,
|
||||||
- preload intent,
|
- preload intent,
|
||||||
- and broad asset family or type where available.
|
- 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
|
### Filters and Search
|
||||||
|
|
||||||
- The baseline filters are:
|
- The baseline filters are:
|
||||||
- `Managed`
|
- `Registered`
|
||||||
- `Orphan`
|
- `Unregistered`
|
||||||
- `Diagnostics`
|
- `Diagnostics`
|
||||||
- `Preload`
|
- `Preload`
|
||||||
- The navigator must support textual search.
|
- 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
|
- asset root path
|
||||||
- path context
|
- 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
|
### Selection Rules
|
||||||
|
|
||||||
- The baseline navigator is single-select.
|
- The baseline navigator is single-select.
|
||||||
- Managed asset selection must be preserved by `asset_id`.
|
- Registered asset selection must be preserved by `asset_id`.
|
||||||
- Orphan selection must be preserved by asset root path until the asset becomes managed.
|
- 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 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.
|
- 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.
|
- 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 assets` state.
|
||||||
- The navigator must define explicit `no results` state.
|
- The navigator must define explicit `no results` state.
|
||||||
- The navigator must define explicit `workspace error` state.
|
- The navigator must define explicit `workspace error` state.
|
||||||
- Orphan assets must appear in the same navigator flow as managed assets.
|
- Unregistered assets must appear in the same navigator flow as registered assets.
|
||||||
- Orphan styling must communicate `declared, not managed`, not `broken`.
|
- Unregistered styling must communicate `present but not yet registered`, not `broken`.
|
||||||
|
|
||||||
### Inputs Expansion
|
### Inputs Expansion
|
||||||
|
|
||||||
@ -137,22 +149,22 @@ Filesystem structure may be visible and actionable as supporting context, but it
|
|||||||
|
|
||||||
## Selected Asset Details Rules
|
## 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`
|
2. `Runtime Contract`
|
||||||
3. `Inputs / Preview`
|
3. `Inputs / Preview`
|
||||||
4. `Diagnostics`
|
4. `Diagnostics`
|
||||||
5. `Actions`
|
|
||||||
|
|
||||||
This section order is stable across asset families.
|
This composition is stable across asset families.
|
||||||
|
|
||||||
### Summary
|
### Summary
|
||||||
|
|
||||||
- `Summary` must always be present for a selected asset.
|
- `Summary` must always be present for a selected asset.
|
||||||
- `Summary` must show:
|
- `Summary` must show:
|
||||||
- `asset_name`
|
- `asset_name`
|
||||||
- managed/orphan state
|
- registration status
|
||||||
|
- build participation
|
||||||
- `asset_id` when available
|
- `asset_id` when available
|
||||||
- family/type
|
- family/type
|
||||||
- asset root path
|
- asset root path
|
||||||
@ -187,16 +199,21 @@ This section order is stable across asset families.
|
|||||||
### Actions
|
### Actions
|
||||||
|
|
||||||
- Actions must be explicit.
|
- 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.
|
- Safe and routine actions must be visually separated from sensitive mutations.
|
||||||
- Hidden or automatic repair behavior is not allowed in the selected-asset view.
|
- Hidden or automatic repair behavior is not allowed in the selected-asset view.
|
||||||
|
|
||||||
## Action Rules
|
## Action Rules
|
||||||
|
|
||||||
- Primary actions for managed assets include `Doctor` and `Build`.
|
- Registered assets excluded from builds must expose an explicit `Include In Build` action.
|
||||||
- Primary actions for orphan assets include `Adopt`.
|
- Primary actions for unregistered assets include `Register`.
|
||||||
- `Register` must remain available when explicit registration is the correct flow.
|
- `Register` is a direct action in Studio UX. If registration is valid, clicking it must register immediately without an extra acknowledgement step.
|
||||||
- Sensitive actions such as `Forget`, `Remove`, and quarantine-like actions must be visually separated from routine actions.
|
- 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.
|
- 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
|
## Activity, Progress, and Logs Rules
|
||||||
|
|
||||||
@ -230,24 +247,25 @@ This section order is stable across asset families.
|
|||||||
|
|
||||||
Sensitive asset mutations are `preview-first`.
|
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
|
### Mutations Requiring Preview
|
||||||
|
|
||||||
- `Adopt`
|
- `Exclude From Build`
|
||||||
- `Forget`
|
|
||||||
- `Remove`
|
- `Remove`
|
||||||
- `Quarantine`
|
|
||||||
- relocational changes such as move or rename of asset roots
|
- relocational changes such as move or rename of asset roots
|
||||||
- batch operations
|
- 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 Content
|
||||||
|
|
||||||
- Preview must show affected assets.
|
- Preview must show affected assets.
|
||||||
|
- Relocation preview must show the planned target root chosen by the user.
|
||||||
- Preview must show proposed actions.
|
- Preview must show proposed actions.
|
||||||
- Preview must distinguish registry impact from workspace impact.
|
- Preview must distinguish registry impact from workspace impact.
|
||||||
- Preview must show blockers, warnings, and safe fixes as separate visual sections.
|
- Preview must show blockers, warnings, and safe fixes as separate visual sections.
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package p.packer.api.assets;
|
package p.packer.api.assets;
|
||||||
|
|
||||||
public enum PackerAssetState {
|
public enum PackerAssetState {
|
||||||
MANAGED,
|
REGISTERED,
|
||||||
ORPHAN,
|
UNREGISTERED
|
||||||
INVALID
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import java.util.Objects;
|
|||||||
public record PackerAssetSummary(
|
public record PackerAssetSummary(
|
||||||
PackerAssetIdentity identity,
|
PackerAssetIdentity identity,
|
||||||
PackerAssetState state,
|
PackerAssetState state,
|
||||||
|
PackerBuildParticipation buildParticipation,
|
||||||
String assetFamily,
|
String assetFamily,
|
||||||
boolean preloadEnabled,
|
boolean preloadEnabled,
|
||||||
boolean hasDiagnostics) {
|
boolean hasDiagnostics) {
|
||||||
@ -12,9 +13,16 @@ public record PackerAssetSummary(
|
|||||||
public PackerAssetSummary {
|
public PackerAssetSummary {
|
||||||
Objects.requireNonNull(identity, "identity");
|
Objects.requireNonNull(identity, "identity");
|
||||||
Objects.requireNonNull(state, "state");
|
Objects.requireNonNull(state, "state");
|
||||||
|
Objects.requireNonNull(buildParticipation, "buildParticipation");
|
||||||
assetFamily = Objects.requireNonNullElse(assetFamily, "unknown").trim();
|
assetFamily = Objects.requireNonNullElse(assetFamily, "unknown").trim();
|
||||||
if (assetFamily.isBlank()) {
|
if (assetFamily.isBlank()) {
|
||||||
assetFamily = "unknown";
|
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 {
|
public enum PackerMutationType {
|
||||||
REGISTER_ASSET,
|
REGISTER_ASSET,
|
||||||
ADOPT_ASSET,
|
INCLUDE_ASSET_IN_BUILD,
|
||||||
FORGET_ASSET,
|
EXCLUDE_ASSET_FROM_BUILD,
|
||||||
REMOVE_ASSET,
|
REMOVE_ASSET,
|
||||||
QUARANTINE_ASSET,
|
|
||||||
RELOCATE_ASSET
|
RELOCATE_ASSET
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package p.packer.building;
|
|||||||
|
|
||||||
import p.packer.api.PackerOperationStatus;
|
import p.packer.api.PackerOperationStatus;
|
||||||
import p.packer.api.PackerProjectContext;
|
import p.packer.api.PackerProjectContext;
|
||||||
|
import p.packer.api.assets.PackerBuildParticipation;
|
||||||
import p.packer.api.assets.PackerAssetState;
|
import p.packer.api.assets.PackerAssetState;
|
||||||
import p.packer.api.diagnostics.PackerDiagnostic;
|
import p.packer.api.diagnostics.PackerDiagnostic;
|
||||||
import p.packer.api.diagnostics.PackerDiagnosticCategory;
|
import p.packer.api.diagnostics.PackerDiagnosticCategory;
|
||||||
@ -45,16 +46,17 @@ public final class PackerBuildPlanner {
|
|||||||
final List<PackerPlannedAsset> plannedAssets = new ArrayList<>();
|
final List<PackerPlannedAsset> plannedAssets = new ArrayList<>();
|
||||||
|
|
||||||
snapshot.assets().stream()
|
snapshot.assets().stream()
|
||||||
.filter(asset -> asset.state() == PackerAssetState.MANAGED)
|
.filter(asset -> asset.buildParticipation() == PackerBuildParticipation.INCLUDED)
|
||||||
.sorted(Comparator.comparingInt(asset -> asset.identity().assetId()))
|
.sorted(Comparator.comparingInt(asset -> asset.identity().assetId()))
|
||||||
.forEach(asset -> {
|
.forEach(asset -> {
|
||||||
final var detailsResult = detailsService.getAssetDetails(new GetAssetDetailsRequest(project, Integer.toString(asset.identity().assetId())));
|
final var detailsResult = detailsService.getAssetDetails(new GetAssetDetailsRequest(project, Integer.toString(asset.identity().assetId())));
|
||||||
diagnostics.addAll(detailsResult.diagnostics());
|
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(
|
diagnostics.add(new PackerDiagnostic(
|
||||||
PackerDiagnosticSeverity.ERROR,
|
PackerDiagnosticSeverity.ERROR,
|
||||||
PackerDiagnosticCategory.BUILD,
|
PackerDiagnosticCategory.BUILD,
|
||||||
"Managed asset is not build-eligible: " + asset.identity().assetName(),
|
"Registered asset is not build-eligible: " + asset.identity().assetName(),
|
||||||
asset.identity().assetRoot(),
|
asset.identity().assetRoot(),
|
||||||
true));
|
true));
|
||||||
return;
|
return;
|
||||||
@ -115,7 +117,7 @@ public final class PackerBuildPlanner {
|
|||||||
"inputs", plannedAssets.stream().map(this::cacheKeyView).toList())));
|
"inputs", plannedAssets.stream().map(this::cacheKeyView).toList())));
|
||||||
return new PackerBuildPlanResult(
|
return new PackerBuildPlanResult(
|
||||||
diagnostics.isEmpty() ? PackerOperationStatus.SUCCESS : PackerOperationStatus.PARTIAL,
|
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),
|
new PackerBuildPlan(cacheKey, plannedAssets, assetTableJson, preloadJson),
|
||||||
List.copyOf(diagnostics));
|
List.copyOf(diagnostics));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import p.packer.api.assets.PackerAssetDetails;
|
|||||||
import p.packer.api.assets.PackerAssetIdentity;
|
import p.packer.api.assets.PackerAssetIdentity;
|
||||||
import p.packer.api.assets.PackerAssetState;
|
import p.packer.api.assets.PackerAssetState;
|
||||||
import p.packer.api.assets.PackerAssetSummary;
|
import p.packer.api.assets.PackerAssetSummary;
|
||||||
|
import p.packer.api.assets.PackerBuildParticipation;
|
||||||
import p.packer.api.diagnostics.PackerDiagnostic;
|
import p.packer.api.diagnostics.PackerDiagnostic;
|
||||||
import p.packer.api.diagnostics.PackerDiagnosticCategory;
|
import p.packer.api.diagnostics.PackerDiagnosticCategory;
|
||||||
import p.packer.api.diagnostics.PackerDiagnosticSeverity;
|
import p.packer.api.diagnostics.PackerDiagnosticSeverity;
|
||||||
@ -65,13 +66,19 @@ public final class PackerAssetDetailsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final PackerAssetDeclaration declaration = parsed.declaration();
|
final PackerAssetDeclaration declaration = parsed.declaration();
|
||||||
|
final PackerAssetState state = resolved.registryEntry().isPresent()
|
||||||
|
? PackerAssetState.REGISTERED
|
||||||
|
: PackerAssetState.UNREGISTERED;
|
||||||
final PackerAssetSummary summary = new PackerAssetSummary(
|
final PackerAssetSummary summary = new PackerAssetSummary(
|
||||||
new PackerAssetIdentity(
|
new PackerAssetIdentity(
|
||||||
resolved.registryEntry().map(PackerRegistryEntry::assetId).orElse(null),
|
resolved.registryEntry().map(PackerRegistryEntry::assetId).orElse(null),
|
||||||
resolved.registryEntry().map(PackerRegistryEntry::assetUuid).orElse(null),
|
resolved.registryEntry().map(PackerRegistryEntry::assetUuid).orElse(null),
|
||||||
declaration.name(),
|
declaration.name(),
|
||||||
resolved.assetRoot()),
|
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.type(),
|
||||||
declaration.preloadEnabled(),
|
declaration.preloadEnabled(),
|
||||||
!diagnostics.isEmpty());
|
!diagnostics.isEmpty());
|
||||||
@ -89,13 +96,19 @@ public final class PackerAssetDetailsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private GetAssetDetailsResult failureResult(ResolvedAssetReference resolved, List<PackerDiagnostic> diagnostics) {
|
private GetAssetDetailsResult failureResult(ResolvedAssetReference resolved, List<PackerDiagnostic> diagnostics) {
|
||||||
|
final PackerAssetState state = resolved.registryEntry().isPresent()
|
||||||
|
? PackerAssetState.REGISTERED
|
||||||
|
: PackerAssetState.UNREGISTERED;
|
||||||
final PackerAssetSummary summary = new PackerAssetSummary(
|
final PackerAssetSummary summary = new PackerAssetSummary(
|
||||||
new PackerAssetIdentity(
|
new PackerAssetIdentity(
|
||||||
resolved.registryEntry().map(PackerRegistryEntry::assetId).orElse(null),
|
resolved.registryEntry().map(PackerRegistryEntry::assetId).orElse(null),
|
||||||
resolved.registryEntry().map(PackerRegistryEntry::assetUuid).orElse(null),
|
resolved.registryEntry().map(PackerRegistryEntry::assetUuid).orElse(null),
|
||||||
resolved.assetRoot().getFileName().toString(),
|
resolved.assetRoot().getFileName().toString(),
|
||||||
resolved.assetRoot()),
|
resolved.assetRoot()),
|
||||||
PackerAssetState.INVALID,
|
state,
|
||||||
|
resolved.registryEntry().map(entry -> entry.includedInBuild()
|
||||||
|
? PackerBuildParticipation.INCLUDED
|
||||||
|
: PackerBuildParticipation.EXCLUDED).orElse(PackerBuildParticipation.EXCLUDED),
|
||||||
"unknown",
|
"unknown",
|
||||||
false,
|
false,
|
||||||
true);
|
true);
|
||||||
|
|||||||
@ -86,38 +86,29 @@ public final class FileSystemPackerDoctorService implements PackerDoctorService
|
|||||||
addDiagnostic(diagnostics, seenDiagnostics, diagnostic);
|
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(
|
addDiagnostic(diagnostics, seenDiagnostics, new PackerDiagnostic(
|
||||||
PackerDiagnosticSeverity.WARNING,
|
PackerDiagnosticSeverity.WARNING,
|
||||||
PackerDiagnosticCategory.HYGIENE,
|
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(),
|
asset.identity().assetRoot(),
|
||||||
false));
|
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()));
|
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 -> {
|
details.inputsByRole().forEach((role, inputs) -> inputs.forEach(input -> {
|
||||||
if (Files.isRegularFile(input)) {
|
if (Files.isRegularFile(input)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final boolean managed = asset.state() == PackerAssetState.MANAGED;
|
final boolean registered = asset.state() == PackerAssetState.REGISTERED;
|
||||||
addDiagnostic(diagnostics, seenDiagnostics, new PackerDiagnostic(
|
addDiagnostic(diagnostics, seenDiagnostics, new PackerDiagnostic(
|
||||||
managed ? PackerDiagnosticSeverity.ERROR : PackerDiagnosticSeverity.WARNING,
|
registered ? PackerDiagnosticSeverity.ERROR : PackerDiagnosticSeverity.WARNING,
|
||||||
managed ? PackerDiagnosticCategory.STRUCTURAL : PackerDiagnosticCategory.HYGIENE,
|
registered ? PackerDiagnosticCategory.STRUCTURAL : PackerDiagnosticCategory.HYGIENE,
|
||||||
"Declared input is missing for role '" + role + "': " + relativeEvidence(project, input),
|
"Declared input is missing for role '" + role + "': " + relativeEvidence(project, input),
|
||||||
input,
|
input,
|
||||||
managed));
|
registered));
|
||||||
}));
|
}));
|
||||||
events.emit(
|
events.emit(
|
||||||
PackerEventKind.PROGRESS_UPDATED,
|
PackerEventKind.PROGRESS_UPDATED,
|
||||||
@ -143,7 +134,7 @@ public final class FileSystemPackerDoctorService implements PackerDoctorService
|
|||||||
if (mode == PackerDoctorMode.EXPANDED_WORKSPACE) {
|
if (mode == PackerDoctorMode.EXPANDED_WORKSPACE) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return state == PackerAssetState.MANAGED || state == PackerAssetState.INVALID;
|
return state == PackerAssetState.REGISTERED;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addDiagnostic(List<PackerDiagnostic> diagnostics, Set<String> seenDiagnostics, PackerDiagnostic diagnostic) {
|
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) {
|
private String relativeAssetRoot(PackerProjectContext project, Path assetRoot) {
|
||||||
return project.rootPath().resolve("assets").toAbsolutePath().normalize()
|
return project.rootPath().resolve("assets").toAbsolutePath().normalize()
|
||||||
.relativize(assetRoot.toAbsolutePath().normalize())
|
.relativize(assetRoot.toAbsolutePath().normalize())
|
||||||
|
|||||||
@ -37,7 +37,11 @@ public final class FileSystemPackerRegistryRepository implements PackerRegistryR
|
|||||||
if (asset == null) {
|
if (asset == null) {
|
||||||
continue;
|
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);
|
validateEntries(entries);
|
||||||
@ -64,7 +68,11 @@ public final class FileSystemPackerRegistryRepository implements PackerRegistryR
|
|||||||
document.schemaVersion = state.schemaVersion();
|
document.schemaVersion = state.schemaVersion();
|
||||||
document.nextAssetId = state.nextAssetId();
|
document.nextAssetId = state.nextAssetId();
|
||||||
document.assets = state.assets().stream()
|
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();
|
.toList();
|
||||||
MAPPER.writerWithDefaultPrettyPrinter().writeValue(registryPath.toFile(), document);
|
MAPPER.writerWithDefaultPrettyPrinter().writeValue(registryPath.toFile(), document);
|
||||||
} catch (IOException exception) {
|
} catch (IOException exception) {
|
||||||
@ -121,6 +129,7 @@ public final class FileSystemPackerRegistryRepository implements PackerRegistryR
|
|||||||
private record RegistryAssetDocument(
|
private record RegistryAssetDocument(
|
||||||
@JsonProperty("asset_id") int assetId,
|
@JsonProperty("asset_id") int assetId,
|
||||||
@JsonProperty("asset_uuid") String assetUuid,
|
@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(
|
public record PackerRegistryEntry(
|
||||||
int assetId,
|
int assetId,
|
||||||
String assetUuid,
|
String assetUuid,
|
||||||
String root) {
|
String root,
|
||||||
|
boolean includedInBuild) {
|
||||||
|
|
||||||
|
public PackerRegistryEntry(int assetId, String assetUuid, String root) {
|
||||||
|
this(assetId, assetUuid, root, true);
|
||||||
|
}
|
||||||
|
|
||||||
public PackerRegistryEntry {
|
public PackerRegistryEntry {
|
||||||
assetUuid = Objects.requireNonNull(assetUuid, "assetUuid").trim();
|
assetUuid = Objects.requireNonNull(assetUuid, "assetUuid").trim();
|
||||||
|
|||||||
@ -36,9 +36,6 @@ import java.util.UUID;
|
|||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
public final class FileSystemPackerMutationService implements PackerMutationService {
|
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 PackerWorkspaceFoundation workspaceFoundation;
|
||||||
private final PackerAssetDetailsService detailsService;
|
private final PackerAssetDetailsService detailsService;
|
||||||
private final PackerProjectWriteCoordinator writeCoordinator;
|
private final PackerProjectWriteCoordinator writeCoordinator;
|
||||||
@ -97,10 +94,10 @@ public final class FileSystemPackerMutationService implements PackerMutationServ
|
|||||||
final Path assetRoot = context.assetDetails().summary().identity().assetRoot();
|
final Path assetRoot = context.assetDetails().summary().identity().assetRoot();
|
||||||
|
|
||||||
return switch (preview.request().type()) {
|
return switch (preview.request().type()) {
|
||||||
case REGISTER_ASSET, ADOPT_ASSET -> applyRegister(project, registry, assetRoot, preview);
|
case REGISTER_ASSET -> applyRegister(project, registry, assetRoot, preview);
|
||||||
case FORGET_ASSET -> applyForget(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 REMOVE_ASSET -> applyRemove(project, registry, assetRoot, preview);
|
||||||
case QUARANTINE_ASSET -> applyQuarantine(project, registry, assetRoot, preview);
|
|
||||||
case RELOCATE_ASSET -> applyRelocate(project, registry, assetRoot, preview);
|
case RELOCATE_ASSET -> applyRelocate(project, registry, assetRoot, preview);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -131,18 +128,26 @@ public final class FileSystemPackerMutationService implements PackerMutationServ
|
|||||||
List.of());
|
List.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
private PackerMutationResult applyForget(
|
private PackerMutationResult applyBuildParticipationChange(
|
||||||
PackerProjectContext project,
|
PackerProjectContext project,
|
||||||
PackerRegistryState registry,
|
PackerRegistryState registry,
|
||||||
Path assetRoot,
|
Path assetRoot,
|
||||||
PackerMutationPreview preview) {
|
PackerMutationPreview preview,
|
||||||
|
boolean includedInBuild) {
|
||||||
final PackerRegistryState updated = registry.withAssets(
|
final PackerRegistryState updated = registry.withAssets(
|
||||||
registry.assets().stream()
|
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(),
|
.toList(),
|
||||||
registry.nextAssetId());
|
registry.nextAssetId());
|
||||||
workspaceFoundation.saveRegistry(project, updated);
|
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(
|
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());
|
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(
|
private PackerMutationResult applyRelocate(
|
||||||
PackerProjectContext project,
|
PackerProjectContext project,
|
||||||
PackerRegistryState registry,
|
PackerRegistryState registry,
|
||||||
@ -184,7 +174,11 @@ public final class FileSystemPackerMutationService implements PackerMutationServ
|
|||||||
final PackerRegistryState updated = registry.withAssets(
|
final PackerRegistryState updated = registry.withAssets(
|
||||||
registry.assets().stream()
|
registry.assets().stream()
|
||||||
.map(entry -> PackerWorkspacePaths.assetRoot(project, entry.root()).equals(assetRoot)
|
.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)
|
: entry)
|
||||||
.toList(),
|
.toList(),
|
||||||
registry.nextAssetId());
|
registry.nextAssetId());
|
||||||
@ -206,11 +200,11 @@ public final class FileSystemPackerMutationService implements PackerMutationServ
|
|||||||
Path targetAssetRoot = null;
|
Path targetAssetRoot = null;
|
||||||
|
|
||||||
switch (request.type()) {
|
switch (request.type()) {
|
||||||
case REGISTER_ASSET, ADOPT_ASSET -> {
|
case REGISTER_ASSET -> {
|
||||||
if (managed) {
|
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.");
|
blockers.add("Asset declaration must be valid before registration.");
|
||||||
}
|
}
|
||||||
if (!blockers.isEmpty()) {
|
if (!blockers.isEmpty()) {
|
||||||
@ -221,12 +215,24 @@ public final class FileSystemPackerMutationService implements PackerMutationServ
|
|||||||
warnings.add("Asset currently reports diagnostics and will still be registered.");
|
warnings.add("Asset currently reports diagnostics and will still be registered.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case FORGET_ASSET -> {
|
case INCLUDE_ASSET_IN_BUILD -> {
|
||||||
if (!managed) {
|
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 {
|
} else {
|
||||||
actions.add(new PackerProposedAction(PackerOperationClass.REGISTRY_MUTATION, "REMOVE", relativeRoot));
|
actions.add(new PackerProposedAction(PackerOperationClass.REGISTRY_MUTATION, "UPDATE", relativeRoot));
|
||||||
warnings.add("The asset will leave the managed build set.");
|
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 -> {
|
case REMOVE_ASSET -> {
|
||||||
@ -236,24 +242,6 @@ public final class FileSystemPackerMutationService implements PackerMutationServ
|
|||||||
actions.add(new PackerProposedAction(PackerOperationClass.WORKSPACE_MUTATION, "DELETE", relativeRoot));
|
actions.add(new PackerProposedAction(PackerOperationClass.WORKSPACE_MUTATION, "DELETE", relativeRoot));
|
||||||
warnings.add("Physical files inside the asset root will be deleted.");
|
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 -> {
|
case RELOCATE_ASSET -> {
|
||||||
targetAssetRoot = request.targetRoot() != null
|
targetAssetRoot = request.targetRoot() != null
|
||||||
? request.targetRoot()
|
? request.targetRoot()
|
||||||
@ -291,26 +279,15 @@ public final class FileSystemPackerMutationService implements PackerMutationServ
|
|||||||
final List<String> initialBlockers = assetDetails.diagnostics().stream()
|
final List<String> initialBlockers = assetDetails.diagnostics().stream()
|
||||||
.filter(PackerDiagnostic::blocking)
|
.filter(PackerDiagnostic::blocking)
|
||||||
.map(PackerDiagnostic::message)
|
.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.REMOVE_ASSET
|
||||||
&& request.type() != PackerMutationType.QUARANTINE_ASSET
|
|
||||||
&& request.type() != PackerMutationType.RELOCATE_ASSET)
|
&& request.type() != PackerMutationType.RELOCATE_ASSET)
|
||||||
.toList();
|
.toList();
|
||||||
return new ResolvedMutationContext(request.project(), request, assetDetails, initialBlockers);
|
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) {
|
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();
|
final Path siblingParent = assetRoot.getParent() == null ? PackerWorkspacePaths.assetsRoot(project) : assetRoot.getParent();
|
||||||
return nextAvailablePath(siblingParent, assetRoot.getFileName().toString() + "-relocated");
|
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.PackerAssetIdentity;
|
||||||
import p.packer.api.assets.PackerAssetState;
|
import p.packer.api.assets.PackerAssetState;
|
||||||
import p.packer.api.assets.PackerAssetSummary;
|
import p.packer.api.assets.PackerAssetSummary;
|
||||||
|
import p.packer.api.assets.PackerBuildParticipation;
|
||||||
import p.packer.api.diagnostics.PackerDiagnostic;
|
import p.packer.api.diagnostics.PackerDiagnostic;
|
||||||
import p.packer.api.diagnostics.PackerDiagnosticCategory;
|
import p.packer.api.diagnostics.PackerDiagnosticCategory;
|
||||||
import p.packer.api.diagnostics.PackerDiagnosticSeverity;
|
import p.packer.api.diagnostics.PackerDiagnosticSeverity;
|
||||||
@ -160,9 +161,12 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
|||||||
: "unknown";
|
: "unknown";
|
||||||
final boolean preload = parsed.declaration() != null && parsed.declaration().preloadEnabled();
|
final boolean preload = parsed.declaration() != null && parsed.declaration().preloadEnabled();
|
||||||
final boolean hasDiagnostics = !parsed.diagnostics().isEmpty();
|
final boolean hasDiagnostics = !parsed.diagnostics().isEmpty();
|
||||||
final PackerAssetState state = parsed.valid()
|
final PackerAssetState state = registryEntry == null
|
||||||
? (registryEntry == null ? PackerAssetState.ORPHAN : PackerAssetState.MANAGED)
|
? PackerAssetState.UNREGISTERED
|
||||||
: PackerAssetState.INVALID;
|
: PackerAssetState.REGISTERED;
|
||||||
|
final PackerBuildParticipation buildParticipation = state == PackerAssetState.REGISTERED
|
||||||
|
? (registryEntry.includedInBuild() ? PackerBuildParticipation.INCLUDED : PackerBuildParticipation.EXCLUDED)
|
||||||
|
: PackerBuildParticipation.EXCLUDED;
|
||||||
return new PackerAssetSummary(
|
return new PackerAssetSummary(
|
||||||
new PackerAssetIdentity(
|
new PackerAssetIdentity(
|
||||||
registryEntry == null ? null : registryEntry.assetId(),
|
registryEntry == null ? null : registryEntry.assetId(),
|
||||||
@ -170,6 +174,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
|||||||
assetName,
|
assetName,
|
||||||
assetRoot),
|
assetRoot),
|
||||||
state,
|
state,
|
||||||
|
buildParticipation,
|
||||||
assetFamily,
|
assetFamily,
|
||||||
preload,
|
preload,
|
||||||
hasDiagnostics);
|
hasDiagnostics);
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test;
|
|||||||
import org.junit.jupiter.api.io.TempDir;
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
import p.packer.api.PackerOperationStatus;
|
import p.packer.api.PackerOperationStatus;
|
||||||
import p.packer.api.PackerProjectContext;
|
import p.packer.api.PackerProjectContext;
|
||||||
|
import p.packer.api.assets.PackerBuildParticipation;
|
||||||
import p.packer.api.assets.PackerAssetState;
|
import p.packer.api.assets.PackerAssetState;
|
||||||
import p.packer.api.workspace.GetAssetDetailsRequest;
|
import p.packer.api.workspace.GetAssetDetailsRequest;
|
||||||
import p.packer.testing.PackerFixtureLocator;
|
import p.packer.testing.PackerFixtureLocator;
|
||||||
@ -20,28 +21,30 @@ final class PackerAssetDetailsServiceTest {
|
|||||||
Path tempDir;
|
Path tempDir;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void returnsManagedDetailsForRegisteredAssetReferenceById() throws Exception {
|
void returnsRegisteredDetailsForRegisteredAssetReferenceById() throws Exception {
|
||||||
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("managed"));
|
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("managed"));
|
||||||
final PackerAssetDetailsService service = new PackerAssetDetailsService();
|
final PackerAssetDetailsService service = new PackerAssetDetailsService();
|
||||||
|
|
||||||
final var result = service.getAssetDetails(new GetAssetDetailsRequest(project(projectRoot), "1"));
|
final var result = service.getAssetDetails(new GetAssetDetailsRequest(project(projectRoot), "1"));
|
||||||
|
|
||||||
assertEquals(PackerOperationStatus.SUCCESS, result.status());
|
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("ui_atlas", result.details().summary().identity().assetName());
|
||||||
assertEquals("TILES/indexed_v1", result.details().outputFormat());
|
assertEquals("TILES/indexed_v1", result.details().outputFormat());
|
||||||
assertTrue(result.diagnostics().isEmpty());
|
assertTrue(result.diagnostics().isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void returnsOrphanDetailsForValidUnregisteredRootReference() throws Exception {
|
void returnsUnregisteredDetailsForValidUnregisteredRootReference() throws Exception {
|
||||||
final Path projectRoot = copyFixture("workspaces/orphan-valid", tempDir.resolve("orphan"));
|
final Path projectRoot = copyFixture("workspaces/orphan-valid", tempDir.resolve("orphan"));
|
||||||
final PackerAssetDetailsService service = new PackerAssetDetailsService();
|
final PackerAssetDetailsService service = new PackerAssetDetailsService();
|
||||||
|
|
||||||
final var result = service.getAssetDetails(new GetAssetDetailsRequest(project(projectRoot), "orphans/ui_sounds"));
|
final var result = service.getAssetDetails(new GetAssetDetailsRequest(project(projectRoot), "orphans/ui_sounds"));
|
||||||
|
|
||||||
assertEquals(PackerOperationStatus.SUCCESS, result.status());
|
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());
|
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"));
|
final var result = service.getAssetDetails(new GetAssetDetailsRequest(project(projectRoot), "bad"));
|
||||||
|
|
||||||
assertEquals(PackerOperationStatus.FAILED, result.status());
|
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());
|
assertFalse(result.diagnostics().isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,7 +68,8 @@ final class PackerAssetDetailsServiceTest {
|
|||||||
final var result = service.getAssetDetails(new GetAssetDetailsRequest(project(tempDir.resolve("empty")), "missing/root"));
|
final var result = service.getAssetDetails(new GetAssetDetailsRequest(project(tempDir.resolve("empty")), "missing/root"));
|
||||||
|
|
||||||
assertEquals(PackerOperationStatus.FAILED, result.status());
|
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")));
|
assertTrue(result.diagnostics().stream().anyMatch(diagnostic -> diagnostic.message().contains("could not be resolved")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -40,7 +40,7 @@ final class FileSystemPackerDoctorServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void expandedWorkspaceReportsOrphanAssetsAndRegisterSafeFixes() throws Exception {
|
void expandedWorkspaceReportsUnregisteredAssetsAndRegisterSafeFixes() throws Exception {
|
||||||
final Path projectRoot = createExpandedWorkspace();
|
final Path projectRoot = createExpandedWorkspace();
|
||||||
final FileSystemPackerDoctorService service = new FileSystemPackerDoctorService();
|
final FileSystemPackerDoctorService service = new FileSystemPackerDoctorService();
|
||||||
|
|
||||||
@ -52,7 +52,7 @@ final class FileSystemPackerDoctorServiceTest {
|
|||||||
assertEquals(PackerOperationStatus.PARTIAL, result.status());
|
assertEquals(PackerOperationStatus.PARTIAL, result.status());
|
||||||
assertTrue(result.diagnostics().stream().anyMatch(diagnostic ->
|
assertTrue(result.diagnostics().stream().anyMatch(diagnostic ->
|
||||||
diagnostic.category() == PackerDiagnosticCategory.HYGIENE
|
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"));
|
assertTrue(result.safeFixes().contains("register_asset orphans/ui_sounds"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -25,37 +25,6 @@ final class FileSystemPackerMutationServiceTest {
|
|||||||
@TempDir
|
@TempDir
|
||||||
Path 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
|
@Test
|
||||||
void applyRelocatePreservesIdentityAndUpdatesRegistryRoot() throws Exception {
|
void applyRelocatePreservesIdentityAndUpdatesRegistryRoot() throws Exception {
|
||||||
final Path projectRoot = createManagedAssetProject();
|
final Path projectRoot = createManagedAssetProject();
|
||||||
@ -104,6 +73,27 @@ final class FileSystemPackerMutationServiceTest {
|
|||||||
assertTrue(preview.blockers().stream().anyMatch(message -> message.contains("valid before registration")));
|
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
|
@Test
|
||||||
void emitsFailureLifecycleWhenApplyFails() throws Exception {
|
void emitsFailureLifecycleWhenApplyFails() throws Exception {
|
||||||
final Path projectRoot = createManagedAssetProject();
|
final Path projectRoot = createManagedAssetProject();
|
||||||
@ -151,7 +141,8 @@ final class FileSystemPackerMutationServiceTest {
|
|||||||
{
|
{
|
||||||
"asset_id": 1,
|
"asset_id": 1,
|
||||||
"asset_uuid": "uuid-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 org.junit.jupiter.api.io.TempDir;
|
||||||
import p.packer.api.PackerOperationStatus;
|
import p.packer.api.PackerOperationStatus;
|
||||||
import p.packer.api.PackerProjectContext;
|
import p.packer.api.PackerProjectContext;
|
||||||
|
import p.packer.api.assets.PackerBuildParticipation;
|
||||||
import p.packer.api.assets.PackerAssetState;
|
import p.packer.api.assets.PackerAssetState;
|
||||||
import p.packer.api.events.PackerEvent;
|
import p.packer.api.events.PackerEvent;
|
||||||
import p.packer.api.events.PackerEventKind;
|
import p.packer.api.events.PackerEventKind;
|
||||||
@ -23,7 +24,7 @@ final class FileSystemPackerWorkspaceServiceTest {
|
|||||||
Path tempDir;
|
Path tempDir;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void listsManagedAndOrphanAssetsFromWorkspaceScan() throws Exception {
|
void listsRegisteredAndUnregisteredAssetsFromWorkspaceScan() throws Exception {
|
||||||
final Path projectRoot = copyFixture("workspaces/read-mixed", tempDir.resolve("mixed"));
|
final Path projectRoot = copyFixture("workspaces/read-mixed", tempDir.resolve("mixed"));
|
||||||
final FileSystemPackerWorkspaceService service = new FileSystemPackerWorkspaceService();
|
final FileSystemPackerWorkspaceService service = new FileSystemPackerWorkspaceService();
|
||||||
|
|
||||||
@ -31,8 +32,8 @@ final class FileSystemPackerWorkspaceServiceTest {
|
|||||||
|
|
||||||
assertEquals(PackerOperationStatus.SUCCESS, result.status());
|
assertEquals(PackerOperationStatus.SUCCESS, result.status());
|
||||||
assertEquals(2, result.assets().size());
|
assertEquals(2, result.assets().size());
|
||||||
assertTrue(result.assets().stream().anyMatch(asset -> asset.state() == PackerAssetState.MANAGED));
|
assertTrue(result.assets().stream().anyMatch(asset -> asset.state() == PackerAssetState.REGISTERED));
|
||||||
assertTrue(result.assets().stream().anyMatch(asset -> asset.state() == PackerAssetState.ORPHAN));
|
assertTrue(result.assets().stream().anyMatch(asset -> asset.state() == PackerAssetState.UNREGISTERED));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -54,7 +55,8 @@ final class FileSystemPackerWorkspaceServiceTest {
|
|||||||
final var result = service.listAssets(new ListAssetsRequest(project(projectRoot)));
|
final var result = service.listAssets(new ListAssetsRequest(project(projectRoot)));
|
||||||
|
|
||||||
assertEquals(1, result.assets().size());
|
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());
|
assertTrue(result.assets().getFirst().hasDiagnostics());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -36,7 +36,13 @@ public final class StudioWorkspaceRailControl<T> extends VBox implements StudioC
|
|||||||
button.setPrefSize(44, 44);
|
button.setPrefSize(44, 44);
|
||||||
button.setMinSize(44, 44);
|
button.setMinSize(44, 44);
|
||||||
button.setMaxSize(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.setTooltip(new Tooltip());
|
||||||
button.getTooltip().textProperty().bind(item.label());
|
button.getTooltip().textProperty().bind(item.label());
|
||||||
button.setOnAction(ignored -> Objects.requireNonNull(onSelect, "onSelect").accept(item.id()));
|
button.setOnAction(ignored -> Objects.requireNonNull(onSelect, "onSelect").accept(item.id()));
|
||||||
|
|||||||
@ -79,10 +79,13 @@ public enum I18n {
|
|||||||
|
|
||||||
WORKSPACE_ASSETS("workspace.assets"),
|
WORKSPACE_ASSETS("workspace.assets"),
|
||||||
ASSETS_NAVIGATOR_TITLE("assets.navigator.title"),
|
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_DETAILS_TITLE("assets.details.title"),
|
||||||
ASSETS_SEARCH_PROMPT("assets.search.prompt"),
|
ASSETS_SEARCH_PROMPT("assets.search.prompt"),
|
||||||
ASSETS_FILTER_MANAGED("assets.filter.managed"),
|
ASSETS_FILTER_REGISTERED("assets.filter.registered"),
|
||||||
ASSETS_FILTER_ORPHAN("assets.filter.orphan"),
|
ASSETS_FILTER_UNREGISTERED("assets.filter.unregistered"),
|
||||||
ASSETS_FILTER_DIAGNOSTICS("assets.filter.diagnostics"),
|
ASSETS_FILTER_DIAGNOSTICS("assets.filter.diagnostics"),
|
||||||
ASSETS_FILTER_PRELOAD("assets.filter.preload"),
|
ASSETS_FILTER_PRELOAD("assets.filter.preload"),
|
||||||
ASSETS_STATE_LOADING("assets.state.loading"),
|
ASSETS_STATE_LOADING("assets.state.loading"),
|
||||||
@ -90,8 +93,8 @@ public enum I18n {
|
|||||||
ASSETS_STATE_NO_RESULTS("assets.state.noResults"),
|
ASSETS_STATE_NO_RESULTS("assets.state.noResults"),
|
||||||
ASSETS_STATE_READY("assets.state.ready"),
|
ASSETS_STATE_READY("assets.state.ready"),
|
||||||
ASSETS_STATE_ERROR("assets.state.error"),
|
ASSETS_STATE_ERROR("assets.state.error"),
|
||||||
ASSETS_BADGE_MANAGED("assets.badge.managed"),
|
ASSETS_BADGE_REGISTERED("assets.badge.registered"),
|
||||||
ASSETS_BADGE_ORPHAN("assets.badge.orphan"),
|
ASSETS_BADGE_UNREGISTERED("assets.badge.unregistered"),
|
||||||
ASSETS_BADGE_PRELOAD("assets.badge.preload"),
|
ASSETS_BADGE_PRELOAD("assets.badge.preload"),
|
||||||
ASSETS_BADGE_DIAGNOSTICS("assets.badge.diagnostics"),
|
ASSETS_BADGE_DIAGNOSTICS("assets.badge.diagnostics"),
|
||||||
ASSETS_SECTION_SUMMARY("assets.section.summary"),
|
ASSETS_SECTION_SUMMARY("assets.section.summary"),
|
||||||
@ -99,15 +102,10 @@ public enum I18n {
|
|||||||
ASSETS_SECTION_INPUTS_PREVIEW("assets.section.inputsPreview"),
|
ASSETS_SECTION_INPUTS_PREVIEW("assets.section.inputsPreview"),
|
||||||
ASSETS_SECTION_DIAGNOSTICS("assets.section.diagnostics"),
|
ASSETS_SECTION_DIAGNOSTICS("assets.section.diagnostics"),
|
||||||
ASSETS_SECTION_ACTIONS("assets.section.actions"),
|
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_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_RELOCATE("assets.action.relocate"),
|
||||||
ASSETS_ACTION_FORGET("assets.action.forget"),
|
|
||||||
ASSETS_ACTION_REMOVE("assets.action.remove"),
|
ASSETS_ACTION_REMOVE("assets.action.remove"),
|
||||||
ASSETS_MUTATION_PREVIEW_TITLE("assets.mutation.previewTitle"),
|
ASSETS_MUTATION_PREVIEW_TITLE("assets.mutation.previewTitle"),
|
||||||
ASSETS_MUTATION_SECTION_CHANGES("assets.mutation.section.changes"),
|
ASSETS_MUTATION_SECTION_CHANGES("assets.mutation.section.changes"),
|
||||||
@ -127,15 +125,20 @@ public enum I18n {
|
|||||||
ASSETS_MUTATION_APPLY("assets.mutation.apply"),
|
ASSETS_MUTATION_APPLY("assets.mutation.apply"),
|
||||||
ASSETS_MUTATION_CONFIRM_TITLE("assets.mutation.confirm.title"),
|
ASSETS_MUTATION_CONFIRM_TITLE("assets.mutation.confirm.title"),
|
||||||
ASSETS_MUTATION_CONFIRM_HEADER("assets.mutation.confirm.header"),
|
ASSETS_MUTATION_CONFIRM_HEADER("assets.mutation.confirm.header"),
|
||||||
ASSETS_MUTATION_CONFIRM_BODY("assets.mutation.confirm.body"),
|
|
||||||
ASSETS_LABEL_NAME("assets.label.name"),
|
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_ASSET_ID("assets.label.assetId"),
|
||||||
ASSETS_LABEL_TYPE("assets.label.type"),
|
ASSETS_LABEL_TYPE("assets.label.type"),
|
||||||
ASSETS_LABEL_LOCATION("assets.label.location"),
|
ASSETS_LABEL_LOCATION("assets.label.location"),
|
||||||
|
ASSETS_LABEL_TARGET_LOCATION("assets.label.targetLocation"),
|
||||||
ASSETS_LABEL_FORMAT("assets.label.format"),
|
ASSETS_LABEL_FORMAT("assets.label.format"),
|
||||||
ASSETS_LABEL_CODEC("assets.label.codec"),
|
ASSETS_LABEL_CODEC("assets.label.codec"),
|
||||||
ASSETS_LABEL_PRELOAD("assets.label.preload"),
|
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_YES("assets.value.yes"),
|
||||||
ASSETS_VALUE_NO("assets.value.no"),
|
ASSETS_VALUE_NO("assets.value.no"),
|
||||||
ASSETS_PROGRESS_IDLE("assets.progress.idle"),
|
ASSETS_PROGRESS_IDLE("assets.progress.idle"),
|
||||||
@ -160,6 +163,49 @@ public enum I18n {
|
|||||||
ASSETS_DETAILS_EMPTY("assets.details.empty"),
|
ASSETS_DETAILS_EMPTY("assets.details.empty"),
|
||||||
ASSETS_DETAILS_READY("assets.details.ready"),
|
ASSETS_DETAILS_READY("assets.details.ready"),
|
||||||
ASSETS_DETAILS_NO_SELECTION("assets.details.noSelection"),
|
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"),
|
WORKSPACE_DEBUG("workspace.debug"),
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -81,16 +81,20 @@ public final class NewProjectWizard {
|
|||||||
feedbackLabel.setWrapText(true);
|
feedbackLabel.setWrapText(true);
|
||||||
|
|
||||||
backButton.textProperty().bind(Container.i18n().bind(I18n.WIZARD_BACK));
|
backButton.textProperty().bind(Container.i18n().bind(I18n.WIZARD_BACK));
|
||||||
|
backButton.getStyleClass().addAll("studio-button", "studio-button-secondary");
|
||||||
backButton.setOnAction(ignored -> goBack());
|
backButton.setOnAction(ignored -> goBack());
|
||||||
|
|
||||||
nextButton.textProperty().bind(Container.i18n().bind(I18n.WIZARD_NEXT));
|
nextButton.textProperty().bind(Container.i18n().bind(I18n.WIZARD_NEXT));
|
||||||
|
nextButton.getStyleClass().addAll("studio-button", "studio-button-primary");
|
||||||
nextButton.setOnAction(ignored -> goNext());
|
nextButton.setOnAction(ignored -> goNext());
|
||||||
|
|
||||||
createButton.textProperty().bind(Container.i18n().bind(I18n.WIZARD_CREATE));
|
createButton.textProperty().bind(Container.i18n().bind(I18n.WIZARD_CREATE));
|
||||||
|
createButton.getStyleClass().addAll("studio-button", "studio-button-primary");
|
||||||
createButton.setOnAction(ignored -> finishCreate());
|
createButton.setOnAction(ignored -> finishCreate());
|
||||||
|
|
||||||
final Button cancelButton = new Button();
|
final Button cancelButton = new Button();
|
||||||
cancelButton.textProperty().bind(Container.i18n().bind(I18n.WIZARD_CANCEL));
|
cancelButton.textProperty().bind(Container.i18n().bind(I18n.WIZARD_CANCEL));
|
||||||
|
cancelButton.getStyleClass().addAll("studio-button", "studio-button-cancel");
|
||||||
cancelButton.setOnAction(ignored -> stage.close());
|
cancelButton.setOnAction(ignored -> stage.close());
|
||||||
|
|
||||||
final HBox actions = new HBox(12, backButton, nextButton, createButton, cancelButton);
|
final HBox actions = new HBox(12, backButton, nextButton, createButton, cancelButton);
|
||||||
@ -155,6 +159,7 @@ public final class NewProjectWizard {
|
|||||||
|
|
||||||
final Button browseButton = new Button();
|
final Button browseButton = new Button();
|
||||||
browseButton.textProperty().bind(Container.i18n().bind(I18n.WIZARD_BROWSE));
|
browseButton.textProperty().bind(Container.i18n().bind(I18n.WIZARD_BROWSE));
|
||||||
|
browseButton.getStyleClass().addAll("studio-button", "studio-button-secondary");
|
||||||
browseButton.setOnAction(ignored -> browseForLocation());
|
browseButton.setOnAction(ignored -> browseForLocation());
|
||||||
|
|
||||||
final HBox row = new HBox(12, locationField, browseButton);
|
final HBox row = new HBox(12, locationField, browseButton);
|
||||||
|
|||||||
@ -88,15 +88,18 @@ public final class ProjectLauncherView extends BorderPane {
|
|||||||
|
|
||||||
final Button openButton = new Button();
|
final Button openButton = new Button();
|
||||||
openButton.textProperty().bind(Container.i18n().bind(I18n.LAUNCHER_OPEN_PROJECT));
|
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.disableProperty().bind(projectList.getSelectionModel().selectedItemProperty().isNull());
|
||||||
openButton.setOnAction(ignored -> openSelectedProject());
|
openButton.setOnAction(ignored -> openSelectedProject());
|
||||||
|
|
||||||
final Button addButton = new Button();
|
final Button addButton = new Button();
|
||||||
addButton.textProperty().bind(Container.i18n().bind(I18n.LAUNCHER_ADD_PROJECT));
|
addButton.textProperty().bind(Container.i18n().bind(I18n.LAUNCHER_ADD_PROJECT));
|
||||||
|
addButton.getStyleClass().addAll("studio-button", "studio-button-secondary");
|
||||||
addButton.setOnAction(ignored -> addExistingProject());
|
addButton.setOnAction(ignored -> addExistingProject());
|
||||||
|
|
||||||
final Button forgetButton = new Button();
|
final Button forgetButton = new Button();
|
||||||
forgetButton.textProperty().bind(Container.i18n().bind(I18n.LAUNCHER_FORGET_PROJECT));
|
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.disableProperty().bind(projectList.getSelectionModel().selectedItemProperty().isNull());
|
||||||
forgetButton.setOnAction(ignored -> forgetSelectedProject());
|
forgetButton.setOnAction(ignored -> forgetSelectedProject());
|
||||||
|
|
||||||
@ -109,6 +112,7 @@ public final class ProjectLauncherView extends BorderPane {
|
|||||||
|
|
||||||
final Button createButton = new Button();
|
final Button createButton = new Button();
|
||||||
createButton.textProperty().bind(Container.i18n().bind(I18n.LAUNCHER_CREATE_BUTTON));
|
createButton.textProperty().bind(Container.i18n().bind(I18n.LAUNCHER_CREATE_BUTTON));
|
||||||
|
createButton.getStyleClass().addAll("studio-button", "studio-button-primary");
|
||||||
createButton.setOnAction(ignored -> openCreateWizard());
|
createButton.setOnAction(ignored -> openCreateWizard());
|
||||||
|
|
||||||
final HBox createRow = new HBox(10, createButton);
|
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;
|
package p.studio.workspaces.assets;
|
||||||
|
|
||||||
public enum AssetNavigatorFilter {
|
public enum AssetNavigatorFilter {
|
||||||
MANAGED,
|
REGISTERED,
|
||||||
ORPHAN,
|
UNREGISTERED,
|
||||||
DIAGNOSTICS,
|
DIAGNOSTICS,
|
||||||
PRELOAD
|
PRELOAD
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,11 +44,11 @@ public final class AssetNavigatorProjectionBuilder {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
final boolean includeManaged = filters.contains(AssetNavigatorFilter.MANAGED);
|
final boolean includeRegistered = filters.contains(AssetNavigatorFilter.REGISTERED);
|
||||||
final boolean includeOrphan = filters.contains(AssetNavigatorFilter.ORPHAN);
|
final boolean includeUnregistered = filters.contains(AssetNavigatorFilter.UNREGISTERED);
|
||||||
if (includeManaged || includeOrphan) {
|
if (includeRegistered || includeUnregistered) {
|
||||||
final boolean stateMatches = (includeManaged && asset.state() == AssetWorkspaceAssetState.MANAGED)
|
final boolean stateMatches = (includeRegistered && asset.state() == AssetWorkspaceAssetState.REGISTERED)
|
||||||
|| (includeOrphan && asset.state() == AssetWorkspaceAssetState.ORPHAN);
|
|| (includeUnregistered && asset.state() == AssetWorkspaceAssetState.UNREGISTERED);
|
||||||
if (!stateMatches) {
|
if (!stateMatches) {
|
||||||
return false;
|
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;
|
package p.studio.workspaces.assets;
|
||||||
|
|
||||||
public enum AssetWorkspaceAction {
|
public enum AssetWorkspaceAction {
|
||||||
DOCTOR,
|
|
||||||
BUILD,
|
|
||||||
ADOPT,
|
|
||||||
REGISTER,
|
REGISTER,
|
||||||
QUARANTINE,
|
INCLUDE_IN_BUILD,
|
||||||
|
EXCLUDE_FROM_BUILD,
|
||||||
RELOCATE,
|
RELOCATE,
|
||||||
FORGET,
|
|
||||||
REMOVE
|
REMOVE
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,18 +10,21 @@ public final class AssetWorkspaceActionSetBuilder {
|
|||||||
public static AssetWorkspaceActionSet forAsset(AssetWorkspaceAssetSummary summary) {
|
public static AssetWorkspaceActionSet forAsset(AssetWorkspaceAssetSummary summary) {
|
||||||
Objects.requireNonNull(summary, "summary");
|
Objects.requireNonNull(summary, "summary");
|
||||||
return switch (summary.state()) {
|
return switch (summary.state()) {
|
||||||
case MANAGED -> new AssetWorkspaceActionSet(
|
case REGISTERED -> summary.buildParticipation() == AssetWorkspaceBuildParticipation.INCLUDED
|
||||||
List.of(AssetWorkspaceAction.DOCTOR, AssetWorkspaceAction.BUILD),
|
? new AssetWorkspaceActionSet(
|
||||||
|
List.of(),
|
||||||
|
List.of(
|
||||||
|
AssetWorkspaceAction.EXCLUDE_FROM_BUILD,
|
||||||
|
AssetWorkspaceAction.RELOCATE,
|
||||||
|
AssetWorkspaceAction.REMOVE))
|
||||||
|
: new AssetWorkspaceActionSet(
|
||||||
|
List.of(AssetWorkspaceAction.INCLUDE_IN_BUILD),
|
||||||
List.of(
|
List.of(
|
||||||
AssetWorkspaceAction.FORGET,
|
|
||||||
AssetWorkspaceAction.QUARANTINE,
|
|
||||||
AssetWorkspaceAction.RELOCATE,
|
AssetWorkspaceAction.RELOCATE,
|
||||||
AssetWorkspaceAction.REMOVE));
|
AssetWorkspaceAction.REMOVE));
|
||||||
case ORPHAN -> new AssetWorkspaceActionSet(
|
case UNREGISTERED -> new AssetWorkspaceActionSet(
|
||||||
List.of(AssetWorkspaceAction.ADOPT, AssetWorkspaceAction.REGISTER),
|
List.of(AssetWorkspaceAction.REGISTER),
|
||||||
List.of(
|
List.of(AssetWorkspaceAction.RELOCATE));
|
||||||
AssetWorkspaceAction.QUARANTINE,
|
|
||||||
AssetWorkspaceAction.RELOCATE));
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package p.studio.workspaces.assets;
|
package p.studio.workspaces.assets;
|
||||||
|
|
||||||
public enum AssetWorkspaceAssetState {
|
public enum AssetWorkspaceAssetState {
|
||||||
MANAGED,
|
REGISTERED,
|
||||||
ORPHAN
|
UNREGISTERED
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ public record AssetWorkspaceAssetSummary(
|
|||||||
AssetWorkspaceSelectionKey selectionKey,
|
AssetWorkspaceSelectionKey selectionKey,
|
||||||
String assetName,
|
String assetName,
|
||||||
AssetWorkspaceAssetState state,
|
AssetWorkspaceAssetState state,
|
||||||
|
AssetWorkspaceBuildParticipation buildParticipation,
|
||||||
Integer assetId,
|
Integer assetId,
|
||||||
String assetFamily,
|
String assetFamily,
|
||||||
Path assetRoot,
|
Path assetRoot,
|
||||||
@ -17,13 +18,17 @@ public record AssetWorkspaceAssetSummary(
|
|||||||
Objects.requireNonNull(selectionKey, "selectionKey");
|
Objects.requireNonNull(selectionKey, "selectionKey");
|
||||||
assetName = Objects.requireNonNull(assetName, "assetName").trim();
|
assetName = Objects.requireNonNull(assetName, "assetName").trim();
|
||||||
Objects.requireNonNull(state, "state");
|
Objects.requireNonNull(state, "state");
|
||||||
|
Objects.requireNonNull(buildParticipation, "buildParticipation");
|
||||||
assetFamily = Objects.requireNonNullElse(assetFamily, "unknown").trim();
|
assetFamily = Objects.requireNonNullElse(assetFamily, "unknown").trim();
|
||||||
assetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
|
assetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
|
||||||
if (assetName.isBlank()) {
|
if (assetName.isBlank()) {
|
||||||
throw new IllegalArgumentException("assetName must not be blank");
|
throw new IllegalArgumentException("assetName must not be blank");
|
||||||
}
|
}
|
||||||
if (state == AssetWorkspaceAssetState.MANAGED && assetId == null) {
|
if (state == AssetWorkspaceAssetState.REGISTERED && assetId == null) {
|
||||||
throw new IllegalArgumentException("managed asset must expose assetId");
|
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 p.studio.projects.ProjectReference;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
public interface AssetWorkspaceMutationService {
|
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);
|
void apply(ProjectReference projectReference, AssetWorkspaceMutationPreview preview);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,11 +15,13 @@ import java.util.stream.Stream;
|
|||||||
public final class FileSystemAssetWorkspaceMutationService implements AssetWorkspaceMutationService {
|
public final class FileSystemAssetWorkspaceMutationService implements AssetWorkspaceMutationService {
|
||||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||||
private static final String PROMETEU_DIR = ".prometeu";
|
private static final String PROMETEU_DIR = ".prometeu";
|
||||||
private static final String QUARANTINE_DIR = "quarantine";
|
|
||||||
private static final String RECOVERED_DIR = "recovered";
|
|
||||||
|
|
||||||
@Override
|
@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(projectReference, "projectReference");
|
||||||
Objects.requireNonNull(asset, "asset");
|
Objects.requireNonNull(asset, "asset");
|
||||||
Objects.requireNonNull(action, "action");
|
Objects.requireNonNull(action, "action");
|
||||||
@ -37,45 +39,59 @@ public final class FileSystemAssetWorkspaceMutationService implements AssetWorks
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case ADOPT, REGISTER -> {
|
case REGISTER -> {
|
||||||
if (asset.state() == AssetWorkspaceAssetState.MANAGED) {
|
if (asset.state() == AssetWorkspaceAssetState.REGISTERED) {
|
||||||
blockers.add("Asset is already managed.");
|
blockers.add("Asset is already registered.");
|
||||||
} else {
|
} else {
|
||||||
changes.add(new AssetWorkspaceMutationChange(
|
changes.add(new AssetWorkspaceMutationChange(
|
||||||
AssetWorkspaceMutationChangeScope.REGISTRY,
|
AssetWorkspaceMutationChangeScope.REGISTRY,
|
||||||
"ADD",
|
"ADD",
|
||||||
relativeRoot));
|
relativeRoot));
|
||||||
if (asset.hasDiagnostics()) {
|
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 -> {
|
case INCLUDE_IN_BUILD -> {
|
||||||
if (isInsideQuarantine(asset.assetRoot(), assetsRoot)) {
|
if (asset.state() != AssetWorkspaceAssetState.REGISTERED) {
|
||||||
blockers.add("Asset is already inside quarantine.");
|
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 {
|
} 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(
|
changes.add(new AssetWorkspaceMutationChange(
|
||||||
AssetWorkspaceMutationChangeScope.WORKSPACE,
|
AssetWorkspaceMutationChangeScope.REGISTRY,
|
||||||
"MOVE",
|
"UPDATE",
|
||||||
relativeRoot + " -> " + relativeAssetRoot(targetAssetRoot, assetsRoot)));
|
relativeRoot));
|
||||||
warnings.add("Quarantine is explicit and reversible, but the asset will leave its current workspace location.");
|
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 -> {
|
case RELOCATE -> {
|
||||||
targetAssetRoot = relocationTarget(asset, assetsRoot);
|
targetAssetRoot = targetRoot == null
|
||||||
final String targetRelativeRoot = relativeAssetRoot(targetAssetRoot, assetsRoot);
|
? relocationTarget(asset, assetsRoot)
|
||||||
if (asset.assetRoot().equals(targetAssetRoot)) {
|
: targetRoot.toAbsolutePath().normalize();
|
||||||
blockers.add("Asset is already at the planned relocation target.");
|
final AssetRootValidationResult validation = AssetRelocationTargetValidator.validate(
|
||||||
|
projectReference,
|
||||||
|
asset.assetRoot(),
|
||||||
|
targetAssetRoot);
|
||||||
|
if (!validation.valid()) {
|
||||||
|
blockers.add(validation.message());
|
||||||
} else {
|
} else {
|
||||||
if (asset.state() == AssetWorkspaceAssetState.MANAGED) {
|
targetAssetRoot = validation.assetRoot();
|
||||||
|
final String targetRelativeRoot = validation.normalizedRelativeRoot();
|
||||||
|
if (asset.state() == AssetWorkspaceAssetState.REGISTERED) {
|
||||||
changes.add(new AssetWorkspaceMutationChange(
|
changes.add(new AssetWorkspaceMutationChange(
|
||||||
AssetWorkspaceMutationChangeScope.REGISTRY,
|
AssetWorkspaceMutationChangeScope.REGISTRY,
|
||||||
"UPDATE",
|
"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.");
|
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 -> {
|
case REMOVE -> {
|
||||||
if (asset.state() == AssetWorkspaceAssetState.MANAGED) {
|
if (asset.state() == AssetWorkspaceAssetState.REGISTERED) {
|
||||||
changes.add(new AssetWorkspaceMutationChange(
|
changes.add(new AssetWorkspaceMutationChange(
|
||||||
AssetWorkspaceMutationChangeScope.REGISTRY,
|
AssetWorkspaceMutationChangeScope.REGISTRY,
|
||||||
"REMOVE",
|
"REMOVE",
|
||||||
@ -112,7 +117,6 @@ public final class FileSystemAssetWorkspaceMutationService implements AssetWorks
|
|||||||
relativeRoot));
|
relativeRoot));
|
||||||
warnings.add("Physical files inside the asset root will be deleted.");
|
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;
|
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();
|
final Path assetRoot = preview.asset().assetRoot();
|
||||||
|
|
||||||
switch (preview.action()) {
|
switch (preview.action()) {
|
||||||
case ADOPT, REGISTER -> {
|
case REGISTER -> {
|
||||||
if (registryContainsRoot(registry, assetRoot, assetsRoot)) {
|
if (registryContainsRoot(registry, assetRoot, assetsRoot)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -145,19 +149,17 @@ public final class FileSystemAssetWorkspaceMutationService implements AssetWorks
|
|||||||
registry.nextAssetId = entry.assetId + 1;
|
registry.nextAssetId = entry.assetId + 1;
|
||||||
writeRegistry(assetsRoot, registry);
|
writeRegistry(assetsRoot, registry);
|
||||||
}
|
}
|
||||||
case FORGET -> {
|
case INCLUDE_IN_BUILD, EXCLUDE_FROM_BUILD -> {
|
||||||
registry.assets.removeIf(entry -> entry.root != null && assetsRoot.resolve(entry.root).toAbsolutePath().normalize().equals(assetRoot));
|
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);
|
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 -> {
|
case RELOCATE -> {
|
||||||
final Path targetAssetRoot = requireTargetAssetRoot(preview);
|
final Path targetAssetRoot = requireTargetAssetRoot(preview);
|
||||||
if (preview.asset().state() == AssetWorkspaceAssetState.MANAGED) {
|
if (preview.asset().state() == AssetWorkspaceAssetState.REGISTERED) {
|
||||||
registry.assets.stream()
|
registry.assets.stream()
|
||||||
.filter(entry -> entry.root != null && assetsRoot.resolve(entry.root).toAbsolutePath().normalize().equals(assetRoot))
|
.filter(entry -> entry.root != null && assetsRoot.resolve(entry.root).toAbsolutePath().normalize().equals(assetRoot))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
@ -171,8 +173,6 @@ public final class FileSystemAssetWorkspaceMutationService implements AssetWorks
|
|||||||
writeRegistry(assetsRoot, registry);
|
writeRegistry(assetsRoot, registry);
|
||||||
deleteRecursively(assetRoot);
|
deleteRecursively(assetRoot);
|
||||||
}
|
}
|
||||||
case DOCTOR, BUILD -> {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,34 +223,12 @@ public final class FileSystemAssetWorkspaceMutationService implements AssetWorks
|
|||||||
return assetsRoot.relativize(assetRoot.toAbsolutePath().normalize()).toString().replace('\\', '/');
|
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) {
|
private Path relocationTarget(AssetWorkspaceAssetSummary asset, Path assetsRoot) {
|
||||||
final Path assetRoot = asset.assetRoot();
|
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();
|
final Path siblingParent = assetRoot.getParent() == null ? assetsRoot : assetRoot.getParent();
|
||||||
return nextAvailableSibling(siblingParent, assetRoot.getFileName().toString() + "-relocated");
|
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) {
|
private Path nextAvailableSibling(Path parent, String baseName) {
|
||||||
final Path normalizedParent = parent.toAbsolutePath().normalize();
|
final Path normalizedParent = parent.toAbsolutePath().normalize();
|
||||||
Path candidate = normalizedParent.resolve(baseName);
|
Path candidate = normalizedParent.resolve(baseName);
|
||||||
@ -262,16 +240,6 @@ public final class FileSystemAssetWorkspaceMutationService implements AssetWorks
|
|||||||
return candidate;
|
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) {
|
private Path requireTargetAssetRoot(AssetWorkspaceMutationPreview preview) {
|
||||||
if (preview.targetAssetRoot() == null) {
|
if (preview.targetAssetRoot() == null) {
|
||||||
throw new IllegalStateException("Mutation preview does not define a target asset root");
|
throw new IllegalStateException("Mutation preview does not define a target asset root");
|
||||||
@ -330,5 +298,8 @@ public final class FileSystemAssetWorkspaceMutationService implements AssetWorks
|
|||||||
|
|
||||||
@JsonProperty("root")
|
@JsonProperty("root")
|
||||||
public String 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());
|
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) ->
|
try (Stream<Path> paths = Files.find(assetsRoot, Integer.MAX_VALUE, (path, attrs) ->
|
||||||
attrs.isRegularFile() && path.getFileName().toString().equals("asset.json"))) {
|
attrs.isRegularFile() && path.getFileName().toString().equals("asset.json"))) {
|
||||||
final List<AssetWorkspaceAssetSummary> assets = paths
|
final List<AssetWorkspaceAssetSummary> assets = paths
|
||||||
@ -42,7 +42,7 @@ public final class FileSystemAssetWorkspaceService implements AssetWorkspaceServ
|
|||||||
public AssetWorkspaceAssetDetails loadAssetDetails(ProjectReference projectReference, AssetWorkspaceSelectionKey selectionKey) {
|
public AssetWorkspaceAssetDetails loadAssetDetails(ProjectReference projectReference, AssetWorkspaceSelectionKey selectionKey) {
|
||||||
final Path projectRoot = Objects.requireNonNull(projectReference, "projectReference").rootPath();
|
final Path projectRoot = Objects.requireNonNull(projectReference, "projectReference").rootPath();
|
||||||
final Path assetsRoot = projectRoot.resolve("assets");
|
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 assetRoot = resolveAssetRoot(selectionKey, assetsRoot, registryByRoot);
|
||||||
final Path assetManifestPath = assetRoot.resolve("asset.json");
|
final Path assetManifestPath = assetRoot.resolve("asset.json");
|
||||||
try {
|
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();
|
final Path assetRoot = assetManifestPath.getParent().toAbsolutePath().normalize();
|
||||||
try {
|
try {
|
||||||
final AssetManifest manifest = MAPPER.readValue(assetManifestPath.toFile(), AssetManifest.class);
|
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
|
final AssetWorkspaceAssetState state = assetId == null
|
||||||
? AssetWorkspaceAssetState.ORPHAN
|
? AssetWorkspaceAssetState.UNREGISTERED
|
||||||
: AssetWorkspaceAssetState.MANAGED;
|
: AssetWorkspaceAssetState.REGISTERED;
|
||||||
final AssetWorkspaceSelectionKey selectionKey = assetId == null
|
final AssetWorkspaceSelectionKey selectionKey = assetId == null
|
||||||
? new AssetWorkspaceSelectionKey.OrphanAsset(assetRoot)
|
? new AssetWorkspaceSelectionKey.OrphanAsset(assetRoot)
|
||||||
: new AssetWorkspaceSelectionKey.ManagedAsset(assetId);
|
: new AssetWorkspaceSelectionKey.ManagedAsset(assetId);
|
||||||
@ -87,6 +88,9 @@ public final class FileSystemAssetWorkspaceService implements AssetWorkspaceServ
|
|||||||
selectionKey,
|
selectionKey,
|
||||||
assetName,
|
assetName,
|
||||||
state,
|
state,
|
||||||
|
state == AssetWorkspaceAssetState.REGISTERED
|
||||||
|
? (registryEntry.includedInBuild() ? AssetWorkspaceBuildParticipation.INCLUDED : AssetWorkspaceBuildParticipation.EXCLUDED)
|
||||||
|
: AssetWorkspaceBuildParticipation.EXCLUDED,
|
||||||
assetId,
|
assetId,
|
||||||
assetFamily,
|
assetFamily,
|
||||||
assetRoot,
|
assetRoot,
|
||||||
@ -97,7 +101,8 @@ public final class FileSystemAssetWorkspaceService implements AssetWorkspaceServ
|
|||||||
return new AssetWorkspaceAssetSummary(
|
return new AssetWorkspaceAssetSummary(
|
||||||
selectionKey,
|
selectionKey,
|
||||||
assetRoot.getFileName().toString(),
|
assetRoot.getFileName().toString(),
|
||||||
AssetWorkspaceAssetState.ORPHAN,
|
AssetWorkspaceAssetState.UNREGISTERED,
|
||||||
|
AssetWorkspaceBuildParticipation.EXCLUDED,
|
||||||
null,
|
null,
|
||||||
"unknown",
|
"unknown",
|
||||||
assetRoot,
|
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");
|
final Path registryPath = assetsRoot.resolve(".prometeu").resolve("index.json");
|
||||||
if (!Files.isRegularFile(registryPath)) {
|
if (!Files.isRegularFile(registryPath)) {
|
||||||
return Map.of();
|
return Map.of();
|
||||||
@ -117,12 +122,12 @@ public final class FileSystemAssetWorkspaceService implements AssetWorkspaceServ
|
|||||||
return Map.of();
|
return Map.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
final Map<Path, Integer> registryByRoot = new HashMap<>();
|
final Map<Path, RegistryEntry> registryByRoot = new HashMap<>();
|
||||||
for (RegistryEntry entry : registry.assets()) {
|
for (RegistryEntry entry : registry.assets()) {
|
||||||
if (entry == null || entry.root() == null || entry.root().isBlank() || entry.assetId() == null) {
|
if (entry == null || entry.root() == null || entry.root().isBlank() || entry.assetId() == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
registryByRoot.put(assetsRoot.resolve(entry.root()).toAbsolutePath().normalize(), entry.assetId());
|
registryByRoot.put(assetsRoot.resolve(entry.root()).toAbsolutePath().normalize(), entry);
|
||||||
}
|
}
|
||||||
return Map.copyOf(registryByRoot);
|
return Map.copyOf(registryByRoot);
|
||||||
} catch (IOException ioException) {
|
} catch (IOException ioException) {
|
||||||
@ -133,10 +138,10 @@ public final class FileSystemAssetWorkspaceService implements AssetWorkspaceServ
|
|||||||
private Path resolveAssetRoot(
|
private Path resolveAssetRoot(
|
||||||
AssetWorkspaceSelectionKey selectionKey,
|
AssetWorkspaceSelectionKey selectionKey,
|
||||||
Path assetsRoot,
|
Path assetsRoot,
|
||||||
Map<Path, Integer> registryByRoot) {
|
Map<Path, RegistryEntry> registryByRoot) {
|
||||||
return switch (selectionKey) {
|
return switch (selectionKey) {
|
||||||
case AssetWorkspaceSelectionKey.ManagedAsset managedAsset -> registryByRoot.entrySet().stream()
|
case AssetWorkspaceSelectionKey.ManagedAsset managedAsset -> registryByRoot.entrySet().stream()
|
||||||
.filter(entry -> managedAsset.assetId() == entry.getValue())
|
.filter(entry -> managedAsset.assetId() == entry.getValue().assetId())
|
||||||
.map(Map.Entry::getKey)
|
.map(Map.Entry::getKey)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElseThrow(() -> new IllegalArgumentException("managed asset root not found for assetId " + managedAsset.assetId()));
|
.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)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
private record RegistryEntry(Integer assetId, String root) {
|
private record RegistryEntry(Integer assetId, String root, boolean includedInBuild) {
|
||||||
private RegistryEntry(
|
private RegistryEntry(
|
||||||
@com.fasterxml.jackson.annotation.JsonProperty("asset_id") Integer assetId,
|
@com.fasterxml.jackson.annotation.JsonProperty("asset_id") Integer assetId,
|
||||||
@com.fasterxml.jackson.annotation.JsonProperty("root") String root) {
|
@com.fasterxml.jackson.annotation.JsonProperty("root") String root,
|
||||||
this.assetId = assetId;
|
@com.fasterxml.jackson.annotation.JsonProperty("included_in_build") Boolean includedInBuild) {
|
||||||
this.root = root;
|
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
|
@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(projectReference, "projectReference");
|
||||||
Objects.requireNonNull(asset, "asset");
|
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(
|
final PackerMutationPreview packerPreview = packerMutationService.preview(
|
||||||
new PackerMutationRequest(
|
new PackerMutationRequest(
|
||||||
project(projectReference),
|
project(projectReference),
|
||||||
mutationType(action),
|
mutationType(action),
|
||||||
assetReference(projectReference, asset),
|
assetReference(projectReference, asset),
|
||||||
null));
|
validatedTargetRoot));
|
||||||
final AssetWorkspaceMutationPreview studioPreview = mapPreview(action, asset, packerPreview);
|
final AssetWorkspaceMutationPreview studioPreview = mapPreview(action, asset, packerPreview);
|
||||||
final OperationSession session = new OperationSession(projectReference, action, studioPreview, packerPreview);
|
final OperationSession session = new OperationSession(projectReference, action, studioPreview, packerPreview);
|
||||||
previewSessions.put(studioPreview, session);
|
previewSessions.put(studioPreview, session);
|
||||||
@ -135,13 +154,11 @@ public final class PackerBackedAssetWorkspaceMutationService implements AssetWor
|
|||||||
|
|
||||||
private PackerMutationType mutationType(AssetWorkspaceAction action) {
|
private PackerMutationType mutationType(AssetWorkspaceAction action) {
|
||||||
return switch (Objects.requireNonNull(action, "action")) {
|
return switch (Objects.requireNonNull(action, "action")) {
|
||||||
case ADOPT -> PackerMutationType.ADOPT_ASSET;
|
|
||||||
case REGISTER -> PackerMutationType.REGISTER_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 RELOCATE -> PackerMutationType.RELOCATE_ASSET;
|
||||||
case FORGET -> PackerMutationType.FORGET_ASSET;
|
|
||||||
case REMOVE -> PackerMutationType.REMOVE_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();
|
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(
|
private record OperationSession(
|
||||||
ProjectReference projectReference,
|
ProjectReference projectReference,
|
||||||
AssetWorkspaceAction action,
|
AssetWorkspaceAction action,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package p.studio.workspaces.assets;
|
package p.studio.workspaces.assets;
|
||||||
|
|
||||||
import p.packer.api.PackerProjectContext;
|
import p.packer.api.PackerProjectContext;
|
||||||
|
import p.packer.api.assets.PackerBuildParticipation;
|
||||||
import p.packer.api.assets.PackerAssetDetails;
|
import p.packer.api.assets.PackerAssetDetails;
|
||||||
import p.packer.api.assets.PackerAssetState;
|
import p.packer.api.assets.PackerAssetState;
|
||||||
import p.packer.api.assets.PackerAssetSummary;
|
import p.packer.api.assets.PackerAssetSummary;
|
||||||
@ -47,13 +48,14 @@ public final class PackerBackedAssetWorkspaceService implements AssetWorkspaceSe
|
|||||||
|
|
||||||
private AssetWorkspaceAssetSummary mapSummary(PackerAssetSummary summary) {
|
private AssetWorkspaceAssetSummary mapSummary(PackerAssetSummary summary) {
|
||||||
final AssetWorkspaceAssetState state = toStudioState(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.ManagedAsset(summary.identity().assetId())
|
||||||
: new AssetWorkspaceSelectionKey.OrphanAsset(summary.identity().assetRoot());
|
: new AssetWorkspaceSelectionKey.OrphanAsset(summary.identity().assetRoot());
|
||||||
return new AssetWorkspaceAssetSummary(
|
return new AssetWorkspaceAssetSummary(
|
||||||
selectionKey,
|
selectionKey,
|
||||||
summary.identity().assetName(),
|
summary.identity().assetName(),
|
||||||
state,
|
state,
|
||||||
|
toBuildParticipation(summary.buildParticipation()),
|
||||||
summary.identity().assetId(),
|
summary.identity().assetId(),
|
||||||
summary.assetFamily(),
|
summary.assetFamily(),
|
||||||
summary.identity().assetRoot(),
|
summary.identity().assetRoot(),
|
||||||
@ -81,10 +83,17 @@ public final class PackerBackedAssetWorkspaceService implements AssetWorkspaceSe
|
|||||||
}
|
}
|
||||||
|
|
||||||
private AssetWorkspaceAssetState toStudioState(PackerAssetSummary summary) {
|
private AssetWorkspaceAssetState toStudioState(PackerAssetSummary summary) {
|
||||||
if (summary.state() == PackerAssetState.MANAGED) {
|
if (summary.state() == PackerAssetState.REGISTERED) {
|
||||||
return AssetWorkspaceAssetState.MANAGED;
|
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) {
|
private PackerProjectContext project(ProjectReference projectReference) {
|
||||||
|
|||||||
@ -53,6 +53,7 @@ public class BuilderWorkspace implements Workspace {
|
|||||||
|
|
||||||
private ToolBar buildToolBar() {
|
private ToolBar buildToolBar() {
|
||||||
buildButton.textProperty().bind(Container.i18n().bind(I18n.WORKSPACE_SHIPPER_BUTTON_RUN));
|
buildButton.textProperty().bind(Container.i18n().bind(I18n.WORKSPACE_SHIPPER_BUTTON_RUN));
|
||||||
|
buildButton.getStyleClass().addAll("studio-button", "studio-button-primary");
|
||||||
buildButton.setOnAction(e -> {
|
buildButton.setOnAction(e -> {
|
||||||
logs.clear();
|
logs.clear();
|
||||||
final var logAggregator = LogAggregator.with(logs::appendText);
|
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.textProperty().bind(Container.i18n().bind(I18n.WORKSPACE_SHIPPER_BUTTON_CLEAR));
|
||||||
|
clearButton.getStyleClass().addAll("studio-button", "studio-button-secondary");
|
||||||
clearButton.setOnAction(e -> logs.clear());
|
clearButton.setOnAction(e -> logs.clear());
|
||||||
|
|
||||||
return new ToolBar(buildButton, clearButton);
|
return new ToolBar(buildButton, clearButton);
|
||||||
|
|||||||
@ -17,7 +17,8 @@ public final class EditorToolbar extends HBox {
|
|||||||
Button saveBtn = iconButton("💾", "Save");
|
Button saveBtn = iconButton("💾", "Save");
|
||||||
|
|
||||||
Button runBtn = iconButton("▶", "Run");
|
Button runBtn = iconButton("▶", "Run");
|
||||||
runBtn.getStyleClass().add("accent");
|
runBtn.getStyleClass().remove("studio-button-secondary");
|
||||||
|
runBtn.getStyleClass().add("studio-button-primary");
|
||||||
|
|
||||||
Region spacer = new Region();
|
Region spacer = new Region();
|
||||||
HBox.setHgrow(spacer, javafx.scene.layout.Priority.ALWAYS);
|
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) {
|
private Button iconButton(String icon, String tooltip) {
|
||||||
Button b = new Button(icon);
|
Button b = new Button(icon);
|
||||||
b.setFocusTraversable(false);
|
b.setFocusTraversable(false);
|
||||||
b.getStyleClass().add("toolbar-button");
|
b.getStyleClass().addAll("studio-button", "studio-button-secondary", "studio-button-icon");
|
||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,19 +69,22 @@ workspace.shipper.button.clear=Clear
|
|||||||
|
|
||||||
workspace.assets=Assets
|
workspace.assets=Assets
|
||||||
assets.navigator.title=Asset Navigator
|
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.details.title=Selected Asset
|
||||||
assets.search.prompt=Search assets by name or path
|
assets.search.prompt=Search assets by name or path
|
||||||
assets.filter.managed=Managed
|
assets.filter.registered=Registered
|
||||||
assets.filter.orphan=Orphan
|
assets.filter.unregistered=Unregistered
|
||||||
assets.filter.diagnostics=Diagnostics
|
assets.filter.diagnostics=Diagnostics
|
||||||
assets.filter.preload=Preload
|
assets.filter.preload=Preload
|
||||||
assets.state.loading=Loading assets...
|
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.noResults=No assets match the current search or filters.
|
||||||
assets.state.ready={0} visible assets ({1} total).
|
assets.state.ready={0} visible assets ({1} total).
|
||||||
assets.state.error=Asset workspace failed to load.
|
assets.state.error=Asset workspace failed to load.
|
||||||
assets.badge.managed=Managed
|
assets.badge.registered=Registered
|
||||||
assets.badge.orphan=Orphan
|
assets.badge.unregistered=Unregistered
|
||||||
assets.badge.preload=Preload
|
assets.badge.preload=Preload
|
||||||
assets.badge.diagnostics=Diagnostics
|
assets.badge.diagnostics=Diagnostics
|
||||||
assets.section.summary=Summary
|
assets.section.summary=Summary
|
||||||
@ -89,15 +92,10 @@ assets.section.runtimeContract=Runtime Contract
|
|||||||
assets.section.inputsPreview=Inputs / Preview
|
assets.section.inputsPreview=Inputs / Preview
|
||||||
assets.section.diagnostics=Diagnostics
|
assets.section.diagnostics=Diagnostics
|
||||||
assets.section.actions=Actions
|
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.register=Register
|
||||||
assets.action.quarantine=Quarantine
|
assets.action.includeInBuild=Include In Build
|
||||||
|
assets.action.excludeFromBuild=Exclude From Build
|
||||||
assets.action.relocate=Relocate
|
assets.action.relocate=Relocate
|
||||||
assets.action.forget=Forget
|
|
||||||
assets.action.remove=Remove
|
assets.action.remove=Remove
|
||||||
assets.mutation.previewTitle=Preview: {0}
|
assets.mutation.previewTitle=Preview: {0}
|
||||||
assets.mutation.section.changes=Changes
|
assets.mutation.section.changes=Changes
|
||||||
@ -115,17 +113,22 @@ assets.mutation.empty.warnings=No warnings.
|
|||||||
assets.mutation.empty.safeFixes=No safe fixes.
|
assets.mutation.empty.safeFixes=No safe fixes.
|
||||||
assets.mutation.cancel=Cancel
|
assets.mutation.cancel=Cancel
|
||||||
assets.mutation.apply=Apply
|
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.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.name=Name
|
||||||
assets.label.state=State
|
assets.label.registration=Registration
|
||||||
|
assets.label.buildParticipation=Build Participation
|
||||||
assets.label.assetId=Asset ID
|
assets.label.assetId=Asset ID
|
||||||
assets.label.type=Type
|
assets.label.type=Type
|
||||||
assets.label.location=Location
|
assets.label.location=Location
|
||||||
|
assets.label.targetLocation=Target Location
|
||||||
assets.label.format=Format
|
assets.label.format=Format
|
||||||
assets.label.codec=Codec
|
assets.label.codec=Codec
|
||||||
assets.label.preload=Preload
|
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.yes=Yes
|
||||||
assets.value.no=No
|
assets.value.no=No
|
||||||
assets.progress.idle=Assets workspace idle.
|
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.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.ready=Selected asset: {0}\nState: {1}\nRoot: {2}
|
||||||
assets.details.noSelection=Select an asset from the navigator once assets are available.
|
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
|
workspace.debug=Debug
|
||||||
|
|||||||
@ -26,21 +26,6 @@
|
|||||||
-fx-border-width: 0 1 0 0;
|
-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 {
|
.studio-right-utility-panel {
|
||||||
-fx-background-color: #1f1f1f;
|
-fx-background-color: #1f1f1f;
|
||||||
-fx-border-color: #2d2d2d;
|
-fx-border-color: #2d2d2d;
|
||||||
@ -108,6 +93,113 @@
|
|||||||
-fx-font-weight: bold;
|
-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 {
|
.studio-utility-placeholder {
|
||||||
-fx-text-fill: #d4d4d4;
|
-fx-text-fill: #d4d4d4;
|
||||||
-fx-padding: 16;
|
-fx-padding: 16;
|
||||||
@ -190,6 +282,12 @@
|
|||||||
-fx-accent: #5cb6ff;
|
-fx-accent: #5cb6ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.assets-workspace-action-bar {
|
||||||
|
-fx-alignment: center-left;
|
||||||
|
-fx-spacing: 8;
|
||||||
|
-fx-padding: 0 0 0 8;
|
||||||
|
}
|
||||||
|
|
||||||
.assets-workspace-pane-title {
|
.assets-workspace-pane-title {
|
||||||
-fx-text-fill: #f3f7fb;
|
-fx-text-fill: #f3f7fb;
|
||||||
-fx-font-size: 15px;
|
-fx-font-size: 15px;
|
||||||
@ -209,21 +307,6 @@
|
|||||||
-fx-padding: 4 0 2 0;
|
-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 {
|
.assets-workspace-summary {
|
||||||
-fx-text-fill: #9ecbff;
|
-fx-text-fill: #9ecbff;
|
||||||
-fx-font-size: 12px;
|
-fx-font-size: 12px;
|
||||||
@ -275,21 +358,91 @@
|
|||||||
-fx-border-color: #4f8dc3;
|
-fx-border-color: #4f8dc3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.assets-workspace-asset-icon {
|
.assets-workspace-asset-row-selected:hover {
|
||||||
-fx-font-size: 14px;
|
-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 {
|
.assets-workspace-asset-name {
|
||||||
-fx-text-fill: #f6fbff;
|
-fx-text-fill: #f6fbff;
|
||||||
-fx-font-size: 13px;
|
-fx-font-size: 15px;
|
||||||
-fx-font-weight: bold;
|
-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 {
|
.assets-workspace-asset-path {
|
||||||
-fx-text-fill: #9eacbb;
|
-fx-text-fill: #9eacbb;
|
||||||
-fx-font-size: 11px;
|
-fx-font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.assets-workspace-asset-badges {
|
||||||
|
-fx-alignment: center-right;
|
||||||
|
-fx-padding: 0 4 0 10;
|
||||||
|
}
|
||||||
|
|
||||||
.assets-workspace-badge {
|
.assets-workspace-badge {
|
||||||
-fx-font-size: 10px;
|
-fx-font-size: 10px;
|
||||||
-fx-padding: 3 7 3 7;
|
-fx-padding: 3 7 3 7;
|
||||||
@ -298,24 +451,12 @@
|
|||||||
-fx-border-width: 1;
|
-fx-border-width: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.assets-workspace-badge-managed {
|
|
||||||
-fx-background-color: #153425;
|
|
||||||
-fx-border-color: #2f8f59;
|
|
||||||
-fx-text-fill: #8ce3ae;
|
|
||||||
}
|
|
||||||
|
|
||||||
.assets-workspace-badge-orphan {
|
.assets-workspace-badge-orphan {
|
||||||
-fx-background-color: #3a2d14;
|
-fx-background-color: #3a2d14;
|
||||||
-fx-border-color: #bc8a31;
|
-fx-border-color: #bc8a31;
|
||||||
-fx-text-fill: #ffd27a;
|
-fx-text-fill: #ffd27a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.assets-workspace-badge-family {
|
|
||||||
-fx-background-color: #1c2733;
|
|
||||||
-fx-border-color: #38506a;
|
|
||||||
-fx-text-fill: #b7d8f8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.assets-workspace-badge-preload {
|
.assets-workspace-badge-preload {
|
||||||
-fx-background-color: #271747;
|
-fx-background-color: #271747;
|
||||||
-fx-border-color: #7f65cf;
|
-fx-border-color: #7f65cf;
|
||||||
@ -348,6 +489,20 @@
|
|||||||
-fx-padding: 4 0 4 0;
|
-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 {
|
.assets-details-section {
|
||||||
-fx-background-color: #11151b;
|
-fx-background-color: #11151b;
|
||||||
-fx-background-radius: 12;
|
-fx-background-radius: 12;
|
||||||
@ -385,6 +540,27 @@
|
|||||||
-fx-font-size: 12px;
|
-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 {
|
.assets-details-role-label {
|
||||||
-fx-text-fill: #9fc3e7;
|
-fx-text-fill: #9fc3e7;
|
||||||
-fx-font-size: 11px;
|
-fx-font-size: 11px;
|
||||||
@ -392,19 +568,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.assets-details-input-button {
|
.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;
|
-fx-alignment: center-left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.assets-details-input-button-selected {
|
|
||||||
-fx-background-color: #224160;
|
|
||||||
-fx-border-color: #4f8dc3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.assets-details-input-preview-split {
|
.assets-details-input-preview-split {
|
||||||
-fx-background-color: transparent;
|
-fx-background-color: transparent;
|
||||||
}
|
}
|
||||||
@ -432,19 +598,6 @@
|
|||||||
-fx-font-weight: bold;
|
-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 {
|
.assets-details-preview-text {
|
||||||
-fx-control-inner-background: #10161d;
|
-fx-control-inner-background: #10161d;
|
||||||
-fx-text-fill: #e4edf6;
|
-fx-text-fill: #e4edf6;
|
||||||
@ -484,25 +637,6 @@
|
|||||||
-fx-font-size: 12px;
|
-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 {
|
.assets-mutation-panel {
|
||||||
-fx-background-color: #0f1318;
|
-fx-background-color: #0f1318;
|
||||||
-fx-background-radius: 12;
|
-fx-background-radius: 12;
|
||||||
@ -543,22 +677,6 @@
|
|||||||
-fx-font-size: 12px;
|
-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 {
|
.assets-workspace-logs-pane {
|
||||||
-fx-collapsible: true;
|
-fx-collapsible: true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,12 +54,12 @@ final class StudioActivityEventMapperTest {
|
|||||||
@Test
|
@Test
|
||||||
void mapsMutationPreviewReadyToInfoEntry() {
|
void mapsMutationPreviewReadyToInfoEntry() {
|
||||||
final StudioActivityEntry entry = StudioActivityEventMapper
|
final StudioActivityEntry entry = StudioActivityEventMapper
|
||||||
.map(new StudioAssetsMutationPreviewReadyEvent(project(), AssetWorkspaceAction.QUARANTINE, 1))
|
.map(new StudioAssetsMutationPreviewReadyEvent(project(), AssetWorkspaceAction.RELOCATE, 1))
|
||||||
.orElseThrow();
|
.orElseThrow();
|
||||||
|
|
||||||
assertEquals("Assets", entry.source());
|
assertEquals("Assets", entry.source());
|
||||||
assertEquals(StudioActivityEntrySeverity.INFO, entry.severity());
|
assertEquals(StudioActivityEntrySeverity.INFO, entry.severity());
|
||||||
assertEquals("Preview ready: quarantine", entry.message());
|
assertEquals("Preview ready: relocate", entry.message());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 Path assetsRoot = projectRoot.resolve("assets");
|
||||||
final AssetNavigatorProjection projection = AssetNavigatorProjectionBuilder.build(
|
final AssetNavigatorProjection projection = AssetNavigatorProjectionBuilder.build(
|
||||||
List.of(
|
List.of(
|
||||||
managedAsset(1, "ui_atlas", "image_bank", assetsRoot.resolve("ui/atlas"), true, false),
|
registeredAsset(1, "ui_atlas", "image_bank", assetsRoot.resolve("ui/atlas"), true, false),
|
||||||
orphanAsset("menu_sounds", "sound_bank", assetsRoot.resolve("audio/menu"), false, false)),
|
unregisteredAsset("menu_sounds", "sound_bank", assetsRoot.resolve("audio/menu"), false, false)),
|
||||||
projectRoot,
|
projectRoot,
|
||||||
"",
|
"",
|
||||||
EnumSet.noneOf(AssetNavigatorFilter.class));
|
EnumSet.noneOf(AssetNavigatorFilter.class));
|
||||||
@ -26,16 +26,16 @@ final class AssetNavigatorProjectionBuilderTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void managedAndOrphanFiltersBehaveAsStateFilterSet() {
|
void registeredAndUnregisteredFiltersBehaveAsStateFilterSet() {
|
||||||
final Path projectRoot = Path.of("/tmp/project");
|
final Path projectRoot = Path.of("/tmp/project");
|
||||||
final Path assetsRoot = projectRoot.resolve("assets");
|
final Path assetsRoot = projectRoot.resolve("assets");
|
||||||
final AssetNavigatorProjection projection = AssetNavigatorProjectionBuilder.build(
|
final AssetNavigatorProjection projection = AssetNavigatorProjectionBuilder.build(
|
||||||
List.of(
|
List.of(
|
||||||
managedAsset(1, "ui_atlas", "image_bank", assetsRoot.resolve("ui/atlas"), true, false),
|
registeredAsset(1, "ui_atlas", "image_bank", assetsRoot.resolve("ui/atlas"), true, false),
|
||||||
orphanAsset("menu_sounds", "sound_bank", assetsRoot.resolve("audio/menu"), false, false)),
|
unregisteredAsset("menu_sounds", "sound_bank", assetsRoot.resolve("audio/menu"), false, false)),
|
||||||
projectRoot,
|
projectRoot,
|
||||||
"",
|
"",
|
||||||
EnumSet.of(AssetNavigatorFilter.MANAGED));
|
EnumSet.of(AssetNavigatorFilter.REGISTERED));
|
||||||
|
|
||||||
assertEquals(1, projection.visibleAssetCount());
|
assertEquals(1, projection.visibleAssetCount());
|
||||||
assertEquals("ui_atlas", projection.groups().getFirst().assets().getFirst().assetName());
|
assertEquals("ui_atlas", projection.groups().getFirst().assets().getFirst().assetName());
|
||||||
@ -47,12 +47,12 @@ final class AssetNavigatorProjectionBuilderTest {
|
|||||||
final Path assetsRoot = projectRoot.resolve("assets");
|
final Path assetsRoot = projectRoot.resolve("assets");
|
||||||
final AssetNavigatorProjection projection = AssetNavigatorProjectionBuilder.build(
|
final AssetNavigatorProjection projection = AssetNavigatorProjectionBuilder.build(
|
||||||
List.of(
|
List.of(
|
||||||
managedAsset(1, "ui_atlas", "image_bank", assetsRoot.resolve("ui/atlas"), true, true),
|
registeredAsset(1, "ui_atlas", "image_bank", assetsRoot.resolve("ui/atlas"), true, true),
|
||||||
managedAsset(2, "bg_tiles", "image_bank", assetsRoot.resolve("bg/tiles"), true, false),
|
registeredAsset(2, "bg_tiles", "image_bank", assetsRoot.resolve("bg/tiles"), true, false),
|
||||||
managedAsset(3, "voice_bank", "sound_bank", assetsRoot.resolve("audio/voice"), false, true)),
|
registeredAsset(3, "voice_bank", "sound_bank", assetsRoot.resolve("audio/voice"), false, true)),
|
||||||
projectRoot,
|
projectRoot,
|
||||||
"",
|
"",
|
||||||
EnumSet.of(AssetNavigatorFilter.MANAGED, AssetNavigatorFilter.PRELOAD, AssetNavigatorFilter.DIAGNOSTICS));
|
EnumSet.of(AssetNavigatorFilter.REGISTERED, AssetNavigatorFilter.PRELOAD, AssetNavigatorFilter.DIAGNOSTICS));
|
||||||
|
|
||||||
assertEquals(1, projection.visibleAssetCount());
|
assertEquals(1, projection.visibleAssetCount());
|
||||||
assertEquals("ui_atlas", projection.groups().getFirst().assets().getFirst().assetName());
|
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 projectRoot = Path.of("/tmp/project");
|
||||||
final Path assetsRoot = projectRoot.resolve("assets");
|
final Path assetsRoot = projectRoot.resolve("assets");
|
||||||
final List<AssetWorkspaceAssetSummary> assets = List.of(
|
final List<AssetWorkspaceAssetSummary> assets = List.of(
|
||||||
managedAsset(1, "ui_atlas", "image_bank", assetsRoot.resolve("ui/atlas"), true, false),
|
registeredAsset(1, "ui_atlas", "image_bank", assetsRoot.resolve("ui/atlas"), true, false),
|
||||||
orphanAsset("menu_sounds", "sound_bank", assetsRoot.resolve("audio/menu"), false, false));
|
unregisteredAsset("menu_sounds", "sound_bank", assetsRoot.resolve("audio/menu"), false, false));
|
||||||
|
|
||||||
final AssetNavigatorProjection byName = AssetNavigatorProjectionBuilder.build(
|
final AssetNavigatorProjection byName = AssetNavigatorProjectionBuilder.build(
|
||||||
assets,
|
assets,
|
||||||
@ -83,7 +83,7 @@ final class AssetNavigatorProjectionBuilderTest {
|
|||||||
assertEquals("menu_sounds", byPath.groups().getFirst().assets().getFirst().assetName());
|
assertEquals("menu_sounds", byPath.groups().getFirst().assets().getFirst().assetName());
|
||||||
}
|
}
|
||||||
|
|
||||||
private AssetWorkspaceAssetSummary managedAsset(
|
private AssetWorkspaceAssetSummary registeredAsset(
|
||||||
int assetId,
|
int assetId,
|
||||||
String name,
|
String name,
|
||||||
String family,
|
String family,
|
||||||
@ -93,7 +93,8 @@ final class AssetNavigatorProjectionBuilderTest {
|
|||||||
return new AssetWorkspaceAssetSummary(
|
return new AssetWorkspaceAssetSummary(
|
||||||
new AssetWorkspaceSelectionKey.ManagedAsset(assetId),
|
new AssetWorkspaceSelectionKey.ManagedAsset(assetId),
|
||||||
name,
|
name,
|
||||||
AssetWorkspaceAssetState.MANAGED,
|
AssetWorkspaceAssetState.REGISTERED,
|
||||||
|
AssetWorkspaceBuildParticipation.INCLUDED,
|
||||||
assetId,
|
assetId,
|
||||||
family,
|
family,
|
||||||
root,
|
root,
|
||||||
@ -101,7 +102,7 @@ final class AssetNavigatorProjectionBuilderTest {
|
|||||||
hasDiagnostics);
|
hasDiagnostics);
|
||||||
}
|
}
|
||||||
|
|
||||||
private AssetWorkspaceAssetSummary orphanAsset(
|
private AssetWorkspaceAssetSummary unregisteredAsset(
|
||||||
String name,
|
String name,
|
||||||
String family,
|
String family,
|
||||||
Path root,
|
Path root,
|
||||||
@ -110,7 +111,8 @@ final class AssetNavigatorProjectionBuilderTest {
|
|||||||
return new AssetWorkspaceAssetSummary(
|
return new AssetWorkspaceAssetSummary(
|
||||||
new AssetWorkspaceSelectionKey.OrphanAsset(root),
|
new AssetWorkspaceSelectionKey.OrphanAsset(root),
|
||||||
name,
|
name,
|
||||||
AssetWorkspaceAssetState.ORPHAN,
|
AssetWorkspaceAssetState.UNREGISTERED,
|
||||||
|
AssetWorkspaceBuildParticipation.EXCLUDED,
|
||||||
null,
|
null,
|
||||||
family,
|
family,
|
||||||
root,
|
root,
|
||||||
|
|||||||
@ -9,40 +9,58 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
|||||||
|
|
||||||
final class AssetWorkspaceActionSetBuilderTest {
|
final class AssetWorkspaceActionSetBuilderTest {
|
||||||
@Test
|
@Test
|
||||||
void managedAssetsExposeDoctorBuildAndSensitiveMutations() {
|
void includedRegisteredAssetsExposeOnlySensitiveAssetMutations() {
|
||||||
final AssetWorkspaceActionSet actionSet = AssetWorkspaceActionSetBuilder.forAsset(new AssetWorkspaceAssetSummary(
|
final AssetWorkspaceActionSet actionSet = AssetWorkspaceActionSetBuilder.forAsset(new AssetWorkspaceAssetSummary(
|
||||||
new AssetWorkspaceSelectionKey.ManagedAsset(42),
|
new AssetWorkspaceSelectionKey.ManagedAsset(42),
|
||||||
"ui_atlas",
|
"ui_atlas",
|
||||||
AssetWorkspaceAssetState.MANAGED,
|
AssetWorkspaceAssetState.REGISTERED,
|
||||||
|
AssetWorkspaceBuildParticipation.INCLUDED,
|
||||||
42,
|
42,
|
||||||
"image_bank",
|
"image_bank",
|
||||||
Path.of("/tmp/assets/ui_atlas"),
|
Path.of("/tmp/assets/ui_atlas"),
|
||||||
true,
|
true,
|
||||||
false));
|
false));
|
||||||
|
|
||||||
assertEquals(List.of(AssetWorkspaceAction.DOCTOR, AssetWorkspaceAction.BUILD), actionSet.primaryActions());
|
assertEquals(List.of(), actionSet.primaryActions());
|
||||||
assertEquals(
|
assertEquals(
|
||||||
List.of(
|
List.of(
|
||||||
AssetWorkspaceAction.FORGET,
|
AssetWorkspaceAction.EXCLUDE_FROM_BUILD,
|
||||||
AssetWorkspaceAction.QUARANTINE,
|
|
||||||
AssetWorkspaceAction.RELOCATE,
|
AssetWorkspaceAction.RELOCATE,
|
||||||
AssetWorkspaceAction.REMOVE),
|
AssetWorkspaceAction.REMOVE),
|
||||||
actionSet.sensitiveActions());
|
actionSet.sensitiveActions());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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(
|
final AssetWorkspaceActionSet actionSet = AssetWorkspaceActionSetBuilder.forAsset(new AssetWorkspaceAssetSummary(
|
||||||
new AssetWorkspaceSelectionKey.OrphanAsset(Path.of("/tmp/assets/ui_sounds")),
|
new AssetWorkspaceSelectionKey.OrphanAsset(Path.of("/tmp/assets/ui_sounds")),
|
||||||
"ui_sounds",
|
"ui_sounds",
|
||||||
AssetWorkspaceAssetState.ORPHAN,
|
AssetWorkspaceAssetState.UNREGISTERED,
|
||||||
|
AssetWorkspaceBuildParticipation.EXCLUDED,
|
||||||
null,
|
null,
|
||||||
"sound_bank",
|
"sound_bank",
|
||||||
Path.of("/tmp/assets/ui_sounds"),
|
Path.of("/tmp/assets/ui_sounds"),
|
||||||
false,
|
false,
|
||||||
false));
|
false));
|
||||||
|
|
||||||
assertEquals(List.of(AssetWorkspaceAction.ADOPT, AssetWorkspaceAction.REGISTER), actionSet.primaryActions());
|
assertEquals(List.of(AssetWorkspaceAction.REGISTER), actionSet.primaryActions());
|
||||||
assertEquals(List.of(AssetWorkspaceAction.QUARANTINE, AssetWorkspaceAction.RELOCATE), actionSet.sensitiveActions());
|
assertEquals(List.of(AssetWorkspaceAction.RELOCATE), actionSet.sensitiveActions());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,8 @@ final class AssetWorkspaceMutationImpactViewModelTest {
|
|||||||
final AssetWorkspaceAssetSummary asset = new AssetWorkspaceAssetSummary(
|
final AssetWorkspaceAssetSummary asset = new AssetWorkspaceAssetSummary(
|
||||||
new AssetWorkspaceSelectionKey.ManagedAsset(1),
|
new AssetWorkspaceSelectionKey.ManagedAsset(1),
|
||||||
"ui_atlas",
|
"ui_atlas",
|
||||||
AssetWorkspaceAssetState.MANAGED,
|
AssetWorkspaceAssetState.REGISTERED,
|
||||||
|
AssetWorkspaceBuildParticipation.INCLUDED,
|
||||||
1,
|
1,
|
||||||
"image_bank",
|
"image_bank",
|
||||||
Path.of("/tmp/assets/ui/atlas"),
|
Path.of("/tmp/assets/ui/atlas"),
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import static org.junit.jupiter.api.Assertions.*;
|
|||||||
|
|
||||||
final class AssetWorkspaceStateTest {
|
final class AssetWorkspaceStateTest {
|
||||||
@Test
|
@Test
|
||||||
void preservesManagedSelectionByAssetIdAcrossRefresh() {
|
void preservesRegisteredSelectionByAssetIdAcrossRefresh() {
|
||||||
final AssetWorkspaceSelectionKey.ManagedAsset selected = new AssetWorkspaceSelectionKey.ManagedAsset(42);
|
final AssetWorkspaceSelectionKey.ManagedAsset selected = new AssetWorkspaceSelectionKey.ManagedAsset(42);
|
||||||
final List<AssetWorkspaceAssetSummary> assets = List.of(
|
final List<AssetWorkspaceAssetSummary> assets = List.of(
|
||||||
managedAsset(42, "ui_atlas", Path.of("/tmp/assets/ui_atlas")),
|
managedAsset(42, "ui_atlas", Path.of("/tmp/assets/ui_atlas")),
|
||||||
@ -24,17 +24,17 @@ final class AssetWorkspaceStateTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void preservesOrphanSelectionByAssetRootAcrossRefresh() {
|
void preservesUnregisteredSelectionByAssetRootAcrossRefresh() {
|
||||||
final Path orphanRoot = Path.of("/tmp/assets/orphan_bank");
|
final Path unregisteredRoot = Path.of("/tmp/assets/orphan_bank");
|
||||||
final AssetWorkspaceSelectionKey.OrphanAsset selected = new AssetWorkspaceSelectionKey.OrphanAsset(orphanRoot);
|
final AssetWorkspaceSelectionKey.OrphanAsset selected = new AssetWorkspaceSelectionKey.OrphanAsset(unregisteredRoot);
|
||||||
final List<AssetWorkspaceAssetSummary> assets = List.of(
|
final List<AssetWorkspaceAssetSummary> assets = List.of(
|
||||||
orphanAsset("orphan_bank", orphanRoot),
|
unregisteredAsset("orphan_bank", unregisteredRoot),
|
||||||
orphanAsset("other_bank", Path.of("/tmp/assets/other_bank")));
|
unregisteredAsset("other_bank", Path.of("/tmp/assets/other_bank")));
|
||||||
|
|
||||||
final AssetWorkspaceState state = AssetWorkspaceState.ready(assets, selected);
|
final AssetWorkspaceState state = AssetWorkspaceState.ready(assets, selected);
|
||||||
|
|
||||||
assertEquals(selected, state.selectedKey());
|
assertEquals(selected, state.selectedKey());
|
||||||
assertEquals(orphanRoot.toAbsolutePath().normalize(), state.selectedAsset().orElseThrow().assetRoot());
|
assertEquals(unregisteredRoot.toAbsolutePath().normalize(), state.selectedAsset().orElseThrow().assetRoot());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -62,7 +62,8 @@ final class AssetWorkspaceStateTest {
|
|||||||
return new AssetWorkspaceAssetSummary(
|
return new AssetWorkspaceAssetSummary(
|
||||||
new AssetWorkspaceSelectionKey.ManagedAsset(assetId),
|
new AssetWorkspaceSelectionKey.ManagedAsset(assetId),
|
||||||
name,
|
name,
|
||||||
AssetWorkspaceAssetState.MANAGED,
|
AssetWorkspaceAssetState.REGISTERED,
|
||||||
|
AssetWorkspaceBuildParticipation.INCLUDED,
|
||||||
assetId,
|
assetId,
|
||||||
"image_bank",
|
"image_bank",
|
||||||
root,
|
root,
|
||||||
@ -70,11 +71,12 @@ final class AssetWorkspaceStateTest {
|
|||||||
false);
|
false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private AssetWorkspaceAssetSummary orphanAsset(String name, Path root) {
|
private AssetWorkspaceAssetSummary unregisteredAsset(String name, Path root) {
|
||||||
return new AssetWorkspaceAssetSummary(
|
return new AssetWorkspaceAssetSummary(
|
||||||
new AssetWorkspaceSelectionKey.OrphanAsset(root),
|
new AssetWorkspaceSelectionKey.OrphanAsset(root),
|
||||||
name,
|
name,
|
||||||
AssetWorkspaceAssetState.ORPHAN,
|
AssetWorkspaceAssetState.UNREGISTERED,
|
||||||
|
AssetWorkspaceBuildParticipation.EXCLUDED,
|
||||||
null,
|
null,
|
||||||
"image_bank",
|
"image_bank",
|
||||||
root,
|
root,
|
||||||
|
|||||||
@ -14,51 +14,42 @@ final class FileSystemAssetWorkspaceMutationServiceTest {
|
|||||||
Path tempDir;
|
Path tempDir;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void previewQuarantineForManagedAssetShowsRegistryAndWorkspaceImpact() throws Exception {
|
void previewRelocateForManagedAssetIsHighRiskAndPreservesIdentityContract() throws Exception {
|
||||||
final Path projectRoot = createManagedAssetProject();
|
final Path projectRoot = createManagedAssetProject();
|
||||||
final FileSystemAssetWorkspaceMutationService service = new FileSystemAssetWorkspaceMutationService();
|
final FileSystemAssetWorkspaceMutationService service = new FileSystemAssetWorkspaceMutationService();
|
||||||
final AssetWorkspaceAssetSummary asset = managedAsset(projectRoot);
|
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());
|
assertTrue(preview.canApply());
|
||||||
assertFalse(preview.highRisk());
|
assertTrue(preview.highRisk());
|
||||||
assertNotNull(preview.targetAssetRoot());
|
assertEquals(customTarget.toAbsolutePath().normalize(), preview.targetAssetRoot());
|
||||||
assertEquals(1, preview.changes().stream().filter(change -> change.scope() == AssetWorkspaceMutationChangeScope.REGISTRY).count());
|
assertTrue(preview.changes().stream().anyMatch(change ->
|
||||||
assertEquals(1, preview.changes().stream().filter(change -> change.scope() == AssetWorkspaceMutationChangeScope.WORKSPACE).count());
|
change.scope() == AssetWorkspaceMutationChangeScope.REGISTRY && change.verb().equals("UPDATE")));
|
||||||
|
assertTrue(preview.changes().stream().anyMatch(change ->
|
||||||
|
change.scope() == AssetWorkspaceMutationChangeScope.WORKSPACE && change.verb().equals("MOVE")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void applyQuarantineMovesAssetAndRemovesRegistryEntry() throws Exception {
|
void applyExcludeFromBuildPreservesRegistrationAndUpdatesRegistryState() throws Exception {
|
||||||
final Path projectRoot = createManagedAssetProject();
|
final Path projectRoot = createManagedAssetProject();
|
||||||
final FileSystemAssetWorkspaceMutationService service = new FileSystemAssetWorkspaceMutationService();
|
final FileSystemAssetWorkspaceMutationService service = new FileSystemAssetWorkspaceMutationService();
|
||||||
final AssetWorkspaceAssetSummary asset = managedAsset(projectRoot);
|
final AssetWorkspaceAssetSummary asset = managedAsset(projectRoot);
|
||||||
final ProjectReference project = project("Main", 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);
|
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"));
|
final String registryJson = Files.readString(projectRoot.resolve("assets/.prometeu/index.json"));
|
||||||
assertFalse(registryJson.contains("\"root\" : \"ui/atlas\""));
|
assertTrue(registryJson.contains("\"asset_id\" : 1"));
|
||||||
}
|
assertTrue(registryJson.contains("\"included_in_build\" : false"));
|
||||||
|
|
||||||
@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")));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -67,15 +58,33 @@ final class FileSystemAssetWorkspaceMutationServiceTest {
|
|||||||
final FileSystemAssetWorkspaceMutationService service = new FileSystemAssetWorkspaceMutationService();
|
final FileSystemAssetWorkspaceMutationService service = new FileSystemAssetWorkspaceMutationService();
|
||||||
final AssetWorkspaceAssetSummary asset = managedAsset(projectRoot);
|
final AssetWorkspaceAssetSummary asset = managedAsset(projectRoot);
|
||||||
final ProjectReference project = project("Main", 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);
|
service.apply(project, preview);
|
||||||
|
|
||||||
assertFalse(Files.exists(asset.assetRoot()));
|
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"));
|
final String registryJson = Files.readString(projectRoot.resolve("assets/.prometeu/index.json"));
|
||||||
assertTrue(registryJson.contains("\"asset_id\" : 1"));
|
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
|
@Test
|
||||||
@ -85,14 +94,15 @@ final class FileSystemAssetWorkspaceMutationServiceTest {
|
|||||||
final AssetWorkspaceAssetSummary asset = new AssetWorkspaceAssetSummary(
|
final AssetWorkspaceAssetSummary asset = new AssetWorkspaceAssetSummary(
|
||||||
new AssetWorkspaceSelectionKey.OrphanAsset(projectRoot.resolve("assets/missing")),
|
new AssetWorkspaceSelectionKey.OrphanAsset(projectRoot.resolve("assets/missing")),
|
||||||
"missing_asset",
|
"missing_asset",
|
||||||
AssetWorkspaceAssetState.ORPHAN,
|
AssetWorkspaceAssetState.UNREGISTERED,
|
||||||
|
AssetWorkspaceBuildParticipation.EXCLUDED,
|
||||||
null,
|
null,
|
||||||
"unknown",
|
"unknown",
|
||||||
projectRoot.resolve("assets/missing"),
|
projectRoot.resolve("assets/missing"),
|
||||||
false,
|
false,
|
||||||
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.canApply());
|
||||||
assertFalse(preview.blockers().isEmpty());
|
assertFalse(preview.blockers().isEmpty());
|
||||||
@ -118,7 +128,8 @@ final class FileSystemAssetWorkspaceMutationServiceTest {
|
|||||||
{
|
{
|
||||||
"asset_id": 1,
|
"asset_id": 1,
|
||||||
"asset_uuid": "uuid-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(
|
return new AssetWorkspaceAssetSummary(
|
||||||
new AssetWorkspaceSelectionKey.ManagedAsset(1),
|
new AssetWorkspaceSelectionKey.ManagedAsset(1),
|
||||||
"ui_atlas",
|
"ui_atlas",
|
||||||
AssetWorkspaceAssetState.MANAGED,
|
AssetWorkspaceAssetState.REGISTERED,
|
||||||
|
AssetWorkspaceBuildParticipation.INCLUDED,
|
||||||
1,
|
1,
|
||||||
"image_bank",
|
"image_bank",
|
||||||
projectRoot.resolve("assets/ui/atlas"),
|
projectRoot.resolve("assets/ui/atlas"),
|
||||||
|
|||||||
@ -25,7 +25,7 @@ final class FileSystemAssetWorkspaceServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void marksRegistryRootsAsManagedAndUnregisteredAnchorsAsOrphan() throws Exception {
|
void marksRegistryRootsAsRegisteredAndUnregisteredAnchorsAsExcluded() throws Exception {
|
||||||
final Path projectRoot = tempDir.resolve("main");
|
final Path projectRoot = tempDir.resolve("main");
|
||||||
final Path assetsRoot = projectRoot.resolve("assets");
|
final Path assetsRoot = projectRoot.resolve("assets");
|
||||||
final Path managedRoot = assetsRoot.resolve("ui").resolve("atlas");
|
final Path managedRoot = assetsRoot.resolve("ui").resolve("atlas");
|
||||||
@ -66,21 +66,23 @@ final class FileSystemAssetWorkspaceServiceTest {
|
|||||||
final AssetWorkspaceSnapshot snapshot = service.loadWorkspace(project("Main", projectRoot));
|
final AssetWorkspaceSnapshot snapshot = service.loadWorkspace(project("Main", projectRoot));
|
||||||
|
|
||||||
assertEquals(2, snapshot.assets().size());
|
assertEquals(2, snapshot.assets().size());
|
||||||
final AssetWorkspaceAssetSummary managed = snapshot.assets().stream()
|
final AssetWorkspaceAssetSummary registered = snapshot.assets().stream()
|
||||||
.filter(asset -> asset.assetName().equals("ui_atlas"))
|
.filter(asset -> asset.assetName().equals("ui_atlas"))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElseThrow();
|
.orElseThrow();
|
||||||
final AssetWorkspaceAssetSummary orphan = snapshot.assets().stream()
|
final AssetWorkspaceAssetSummary unregistered = snapshot.assets().stream()
|
||||||
.filter(asset -> asset.assetName().equals("ui_sounds"))
|
.filter(asset -> asset.assetName().equals("ui_sounds"))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElseThrow();
|
.orElseThrow();
|
||||||
|
|
||||||
assertEquals(AssetWorkspaceAssetState.MANAGED, managed.state());
|
assertEquals(AssetWorkspaceAssetState.REGISTERED, registered.state());
|
||||||
assertEquals(1, managed.assetId());
|
assertEquals(AssetWorkspaceBuildParticipation.INCLUDED, registered.buildParticipation());
|
||||||
assertTrue(managed.preload());
|
assertEquals(1, registered.assetId());
|
||||||
|
assertTrue(registered.preload());
|
||||||
|
|
||||||
assertEquals(AssetWorkspaceAssetState.ORPHAN, orphan.state());
|
assertEquals(AssetWorkspaceAssetState.UNREGISTERED, unregistered.state());
|
||||||
assertEquals(null, orphan.assetId());
|
assertEquals(AssetWorkspaceBuildParticipation.EXCLUDED, unregistered.buildParticipation());
|
||||||
|
assertEquals(null, unregistered.assetId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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(
|
final AssetWorkspaceMutationPreview preview = service.preview(
|
||||||
project("Main", projectRoot),
|
project("Main", projectRoot),
|
||||||
managedAsset(projectRoot),
|
managedAsset(projectRoot),
|
||||||
AssetWorkspaceAction.QUARANTINE);
|
AssetWorkspaceAction.RELOCATE,
|
||||||
|
projectRoot.resolve("assets/reorganized/atlas"));
|
||||||
|
|
||||||
assertEquals(1, previewEvents.size());
|
assertEquals(1, previewEvents.size());
|
||||||
assertTrue(preview.canApply());
|
assertTrue(preview.canApply());
|
||||||
@ -53,16 +54,19 @@ final class PackerBackedAssetWorkspaceMutationServiceTest {
|
|||||||
final List<StudioAssetsMutationAppliedEvent> appliedEvents = new ArrayList<>();
|
final List<StudioAssetsMutationAppliedEvent> appliedEvents = new ArrayList<>();
|
||||||
globalBus.subscribe(StudioAssetsMutationAppliedEvent.class, appliedEvents::add);
|
globalBus.subscribe(StudioAssetsMutationAppliedEvent.class, appliedEvents::add);
|
||||||
final PackerBackedAssetWorkspaceMutationService service = service(globalBus);
|
final PackerBackedAssetWorkspaceMutationService service = service(globalBus);
|
||||||
|
final Path customTarget = projectRoot.resolve("assets/reorganized/atlas");
|
||||||
final AssetWorkspaceMutationPreview preview = service.preview(
|
final AssetWorkspaceMutationPreview preview = service.preview(
|
||||||
project("Main", projectRoot),
|
project("Main", projectRoot),
|
||||||
managedAsset(projectRoot),
|
managedAsset(projectRoot),
|
||||||
AssetWorkspaceAction.RELOCATE);
|
AssetWorkspaceAction.RELOCATE,
|
||||||
|
customTarget);
|
||||||
|
|
||||||
service.apply(project("Main", projectRoot), preview);
|
service.apply(project("Main", projectRoot), preview);
|
||||||
|
|
||||||
assertEquals(1, appliedEvents.size());
|
assertEquals(1, appliedEvents.size());
|
||||||
assertEquals(AssetWorkspaceAction.RELOCATE, appliedEvents.getFirst().action());
|
assertEquals(AssetWorkspaceAction.RELOCATE, appliedEvents.getFirst().action());
|
||||||
assertTrue(Files.isDirectory(preview.targetAssetRoot()));
|
assertEquals(customTarget.toAbsolutePath().normalize(), preview.targetAssetRoot());
|
||||||
|
assertTrue(Files.isDirectory(customTarget));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -72,10 +76,12 @@ final class PackerBackedAssetWorkspaceMutationServiceTest {
|
|||||||
final List<StudioAssetsMutationFailedEvent> failedEvents = new ArrayList<>();
|
final List<StudioAssetsMutationFailedEvent> failedEvents = new ArrayList<>();
|
||||||
globalBus.subscribe(StudioAssetsMutationFailedEvent.class, failedEvents::add);
|
globalBus.subscribe(StudioAssetsMutationFailedEvent.class, failedEvents::add);
|
||||||
final PackerBackedAssetWorkspaceMutationService service = service(globalBus);
|
final PackerBackedAssetWorkspaceMutationService service = service(globalBus);
|
||||||
|
final Path customTarget = projectRoot.resolve("assets/reorganized/atlas");
|
||||||
final AssetWorkspaceMutationPreview preview = service.preview(
|
final AssetWorkspaceMutationPreview preview = service.preview(
|
||||||
project("Main", projectRoot),
|
project("Main", projectRoot),
|
||||||
managedAsset(projectRoot),
|
managedAsset(projectRoot),
|
||||||
AssetWorkspaceAction.RELOCATE);
|
AssetWorkspaceAction.RELOCATE,
|
||||||
|
customTarget);
|
||||||
|
|
||||||
deleteRecursively(projectRoot.resolve("assets/ui/atlas"));
|
deleteRecursively(projectRoot.resolve("assets/ui/atlas"));
|
||||||
|
|
||||||
@ -85,6 +91,27 @@ final class PackerBackedAssetWorkspaceMutationServiceTest {
|
|||||||
assertFalse(failedEvents.getFirst().message().isBlank());
|
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) {
|
private PackerBackedAssetWorkspaceMutationService service(StudioEventBus globalBus) {
|
||||||
return new PackerBackedAssetWorkspaceMutationService(
|
return new PackerBackedAssetWorkspaceMutationService(
|
||||||
new StudioWorkspaceEventBus(WorkspaceId.ASSETS, globalBus),
|
new StudioWorkspaceEventBus(WorkspaceId.ASSETS, globalBus),
|
||||||
@ -126,7 +153,8 @@ final class PackerBackedAssetWorkspaceMutationServiceTest {
|
|||||||
return new AssetWorkspaceAssetSummary(
|
return new AssetWorkspaceAssetSummary(
|
||||||
new AssetWorkspaceSelectionKey.ManagedAsset(1),
|
new AssetWorkspaceSelectionKey.ManagedAsset(1),
|
||||||
"ui_atlas",
|
"ui_atlas",
|
||||||
AssetWorkspaceAssetState.MANAGED,
|
AssetWorkspaceAssetState.REGISTERED,
|
||||||
|
AssetWorkspaceBuildParticipation.INCLUDED,
|
||||||
1,
|
1,
|
||||||
"image_bank",
|
"image_bank",
|
||||||
projectRoot.resolve("assets/ui/atlas"),
|
projectRoot.resolve("assets/ui/atlas"),
|
||||||
|
|||||||
@ -60,8 +60,8 @@ final class PackerBackedAssetWorkspaceServiceTest {
|
|||||||
final AssetWorkspaceSnapshot snapshot = service.loadWorkspace(project("Main", projectRoot));
|
final AssetWorkspaceSnapshot snapshot = service.loadWorkspace(project("Main", projectRoot));
|
||||||
|
|
||||||
assertEquals(2, snapshot.assets().size());
|
assertEquals(2, snapshot.assets().size());
|
||||||
assertTrue(snapshot.assets().stream().anyMatch(asset -> asset.state() == AssetWorkspaceAssetState.MANAGED));
|
assertTrue(snapshot.assets().stream().anyMatch(asset -> asset.state() == AssetWorkspaceAssetState.REGISTERED));
|
||||||
assertTrue(snapshot.assets().stream().anyMatch(asset -> asset.state() == AssetWorkspaceAssetState.ORPHAN));
|
assertTrue(snapshot.assets().stream().anyMatch(asset -> asset.state() == AssetWorkspaceAssetState.UNREGISTERED));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@ -69,7 +69,7 @@ final class PackerStudioIntegrationTest {
|
|||||||
assertEquals(2, snapshot.assets().size());
|
assertEquals(2, snapshot.assets().size());
|
||||||
|
|
||||||
final AssetWorkspaceAssetSummary orphan = snapshot.assets().stream()
|
final AssetWorkspaceAssetSummary orphan = snapshot.assets().stream()
|
||||||
.filter(asset -> asset.state() == AssetWorkspaceAssetState.ORPHAN)
|
.filter(asset -> asset.state() == AssetWorkspaceAssetState.UNREGISTERED)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElseThrow();
|
.orElseThrow();
|
||||||
final AssetWorkspaceMutationPreview preview = mutationService.preview(project, orphan, AssetWorkspaceAction.REGISTER);
|
final AssetWorkspaceMutationPreview preview = mutationService.preview(project, orphan, AssetWorkspaceAction.REGISTER);
|
||||||
|
|||||||
@ -1,9 +1,20 @@
|
|||||||
{
|
{
|
||||||
"schema_version" : 1,
|
"schema_version" : 1,
|
||||||
"next_asset_id" : 2,
|
"next_asset_id" : 9,
|
||||||
"assets" : [ {
|
"assets" : [ {
|
||||||
"asset_id" : 1,
|
"asset_id" : 3,
|
||||||
"asset_uuid" : "67cd978d-cd61-4641-ba9e-98fe4bc039bd",
|
"asset_uuid" : "21953cb8-4101-4790-9e5e-d95f5fbc9b5a",
|
||||||
"root" : "ui/atlas-relocated"
|
"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