implements PR-020
This commit is contained in:
parent
60a0b571f8
commit
4278d045c2
@ -0,0 +1,101 @@
|
|||||||
|
# PR-20 Asset Action Capabilities and Register-First Delivery
|
||||||
|
|
||||||
|
Domain Owner: `docs/packer`
|
||||||
|
Cross-Domain Impact: `docs/studio`
|
||||||
|
|
||||||
|
## Briefing
|
||||||
|
|
||||||
|
The current `Assets` workspace still decides too much locally about which asset actions should appear and when they should be allowed.
|
||||||
|
|
||||||
|
That is the wrong long-term boundary.
|
||||||
|
|
||||||
|
The packer already owns asset semantics, registration state, build participation, and write execution. It should also own the capability decision for asset actions, while Studio remains a consumer that renders the actions exposed by the service.
|
||||||
|
|
||||||
|
This PR introduces a capability-based action contract driven by the packer, with `AssetAction.REGISTER` as the first delivered action end to end.
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Add a packer-owned asset action capability API and deliver the first action, `REGISTER`, through the full packer-to-Studio path.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- [`./PR-15-snapshot-backed-asset-query-services.md`](./PR-15-snapshot-backed-asset-query-services.md)
|
||||||
|
- [`./PR-16-write-lane-command-completion-and-used-write-services.md`](./PR-16-write-lane-command-completion-and-used-write-services.md)
|
||||||
|
- [`./PR-17-studio-runtime-adapter-and-assets-workspace-consumption.md`](./PR-17-studio-runtime-adapter-and-assets-workspace-consumption.md)
|
||||||
|
- [`./PR-18-legacy-service-retirement-and-regression-hardening.md`](./PR-18-legacy-service-retirement-and-regression-hardening.md)
|
||||||
|
- [`./PR-19-api-surface-audit-model-separation-and-public-read-dtos.md`](./PR-19-api-surface-audit-model-separation-and-public-read-dtos.md)
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- define a public packer action contract for asset capabilities
|
||||||
|
- let the packer decide which actions are available for a given asset at a given moment
|
||||||
|
- let Studio render the action section from packer-provided capabilities instead of local semantic rules
|
||||||
|
- keep the capability contract stable even if the packer later moves to FSM or another internal decision model
|
||||||
|
- deliver `AssetAction.REGISTER` as the first supported action end to end
|
||||||
|
- keep the service-only wave focused on active functionality
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- no FSM implementation
|
||||||
|
- no full action family rollout in the first delivery
|
||||||
|
- no doctor/build/reconcile action surfaces
|
||||||
|
- no speculative future actions without packer support
|
||||||
|
- no frontend-local capability engine
|
||||||
|
|
||||||
|
## Execution Method
|
||||||
|
|
||||||
|
1. Add a public action capability contract in `prometeu-packer-api`, including:
|
||||||
|
- `AssetAction` enum
|
||||||
|
- capability response shape for asset actions
|
||||||
|
- request/response messages for reading action capabilities
|
||||||
|
2. Model the response so the packer can expose at least:
|
||||||
|
- `action`
|
||||||
|
- `enabled`
|
||||||
|
- `visible`
|
||||||
|
- optional `reason`
|
||||||
|
3. Add a runtime-backed read path in `prometeu-packer-v1` that resolves action capabilities from the current asset state.
|
||||||
|
4. Keep the internal decision logic inside the packer implementation. Studio must not reconstruct capability semantics.
|
||||||
|
5. Add a write path for `AssetAction.REGISTER`.
|
||||||
|
6. Expose `REGISTER` only when the asset capability resolver says it is allowed.
|
||||||
|
7. Render the `Actions` section in Studio from the returned capabilities.
|
||||||
|
8. Execute `REGISTER` from Studio through the packer write path and refresh the asset workspace after completion.
|
||||||
|
|
||||||
|
## Register-First Delivery Rules
|
||||||
|
|
||||||
|
The first delivered action is:
|
||||||
|
|
||||||
|
- `AssetAction.REGISTER`
|
||||||
|
|
||||||
|
Expected initial capability behavior:
|
||||||
|
|
||||||
|
- unregistered assets may expose `REGISTER`
|
||||||
|
- registered assets must not expose `REGISTER`
|
||||||
|
- Studio must only show the `Register` button when the packer returns that capability
|
||||||
|
|
||||||
|
Other future actions such as `MOVE`, `DELETE`, `INCLUDE_IN_BUILD`, `EXCLUDE_FROM_BUILD`, `ENABLE_PRELOAD`, `DISABLE_PRELOAD`, and `CHANGE_CONTRACT` are intentionally deferred from code delivery in this PR unless needed to support the contract shape.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- the packer API exposes an asset action capability contract
|
||||||
|
- Studio reads action capabilities from the packer instead of deciding them locally
|
||||||
|
- the first delivered action is `AssetAction.REGISTER`
|
||||||
|
- `REGISTER` is available only for unregistered assets
|
||||||
|
- `REGISTER` is not shown for registered assets
|
||||||
|
- Studio can trigger `REGISTER` through the packer service path and refresh the workspace after success
|
||||||
|
- the capability contract remains valid if packer internals later migrate to FSM or another orchestration model
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- packer unit tests for capability resolution
|
||||||
|
- packer tests for `REGISTER` execution
|
||||||
|
- Studio tests or smoke tests proving that action buttons are rendered from service-provided capabilities
|
||||||
|
- end-to-end validation for unregistered asset -> register -> refresh -> registered asset
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- `prometeu-packer/prometeu-packer-api/src/main/java/p/packer/**`
|
||||||
|
- `prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/**`
|
||||||
|
- `prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/**`
|
||||||
|
- `prometeu-studio/src/main/java/p/studio/**`
|
||||||
|
- `prometeu-studio/src/test/java/p/studio/**`
|
||||||
|
|
||||||
@ -79,6 +79,7 @@ The current production track for the standalone `prometeu-packer` project is:
|
|||||||
17. [`PR-17-studio-runtime-adapter-and-assets-workspace-consumption.md`](./PR-17-studio-runtime-adapter-and-assets-workspace-consumption.md)
|
17. [`PR-17-studio-runtime-adapter-and-assets-workspace-consumption.md`](./PR-17-studio-runtime-adapter-and-assets-workspace-consumption.md)
|
||||||
18. [`PR-18-legacy-service-retirement-and-regression-hardening.md`](./PR-18-legacy-service-retirement-and-regression-hardening.md)
|
18. [`PR-18-legacy-service-retirement-and-regression-hardening.md`](./PR-18-legacy-service-retirement-and-regression-hardening.md)
|
||||||
19. [`PR-19-api-surface-audit-model-separation-and-public-read-dtos.md`](./PR-19-api-surface-audit-model-separation-and-public-read-dtos.md)
|
19. [`PR-19-api-surface-audit-model-separation-and-public-read-dtos.md`](./PR-19-api-surface-audit-model-separation-and-public-read-dtos.md)
|
||||||
|
20. [`PR-20-asset-action-capabilities-and-register-first-delivery.md`](./PR-20-asset-action-capabilities-and-register-first-delivery.md)
|
||||||
|
|
||||||
Current wave discipline from `PR-11` onward:
|
Current wave discipline from `PR-11` onward:
|
||||||
|
|
||||||
@ -89,4 +90,4 @@ Current wave discipline from `PR-11` onward:
|
|||||||
|
|
||||||
Recommended dependency chain:
|
Recommended dependency chain:
|
||||||
|
|
||||||
`PR-01 -> PR-02 -> PR-03 -> PR-04 -> PR-05 -> PR-06 -> PR-07 -> PR-08 -> PR-09 -> PR-10 -> PR-11 -> PR-12 -> PR-13 -> PR-14 -> PR-15 -> PR-16 -> PR-17 -> PR-18 -> PR-19`
|
`PR-01 -> PR-02 -> PR-03 -> PR-04 -> PR-05 -> PR-06 -> PR-07 -> PR-08 -> PR-09 -> PR-10 -> PR-11 -> PR-12 -> PR-13 -> PR-14 -> PR-15 -> PR-16 -> PR-17 -> PR-18 -> PR-19 -> PR-20`
|
||||||
|
|||||||
@ -9,5 +9,9 @@ public interface PackerWorkspaceService {
|
|||||||
|
|
||||||
GetAssetDetailsResult getAssetDetails(GetAssetDetailsRequest request);
|
GetAssetDetailsResult getAssetDetails(GetAssetDetailsRequest request);
|
||||||
|
|
||||||
|
GetAssetActionsResult getAssetActions(GetAssetActionsRequest request);
|
||||||
|
|
||||||
CreateAssetResult createAsset(CreateAssetRequest request);
|
CreateAssetResult createAsset(CreateAssetRequest request);
|
||||||
|
|
||||||
|
RegisterAssetResult registerAsset(RegisterAssetRequest request);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,20 @@
|
|||||||
|
package p.packer.dtos;
|
||||||
|
|
||||||
|
import p.packer.messages.assets.AssetAction;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record PackerAssetActionAvailabilityDTO(
|
||||||
|
AssetAction action,
|
||||||
|
boolean enabled,
|
||||||
|
boolean visible,
|
||||||
|
String reason) {
|
||||||
|
|
||||||
|
public PackerAssetActionAvailabilityDTO {
|
||||||
|
Objects.requireNonNull(action, "action");
|
||||||
|
reason = reason == null ? null : reason.trim();
|
||||||
|
if (reason != null && reason.isBlank()) {
|
||||||
|
reason = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package p.packer.messages;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record GetAssetActionsRequest(
|
||||||
|
PackerProjectContext project,
|
||||||
|
AssetReference assetReference) {
|
||||||
|
|
||||||
|
public GetAssetActionsRequest {
|
||||||
|
Objects.requireNonNull(project, "project");
|
||||||
|
Objects.requireNonNull(assetReference, "assetReference");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package p.packer.messages;
|
||||||
|
|
||||||
|
import p.packer.dtos.PackerAssetActionAvailabilityDTO;
|
||||||
|
import p.packer.dtos.PackerDiagnosticDTO;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record GetAssetActionsResult(
|
||||||
|
PackerOperationStatus status,
|
||||||
|
String summary,
|
||||||
|
List<PackerAssetActionAvailabilityDTO> actions,
|
||||||
|
List<PackerDiagnosticDTO> diagnostics) {
|
||||||
|
|
||||||
|
public GetAssetActionsResult {
|
||||||
|
Objects.requireNonNull(status, "status");
|
||||||
|
summary = Objects.requireNonNull(summary, "summary").trim();
|
||||||
|
actions = List.copyOf(Objects.requireNonNull(actions, "actions"));
|
||||||
|
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
|
||||||
|
if (summary.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("summary must not be blank");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package p.packer.messages;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record RegisterAssetRequest(
|
||||||
|
PackerProjectContext project,
|
||||||
|
AssetReference assetReference) {
|
||||||
|
|
||||||
|
public RegisterAssetRequest {
|
||||||
|
Objects.requireNonNull(project, "project");
|
||||||
|
Objects.requireNonNull(assetReference, "assetReference");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
package p.packer.messages;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record RegisterAssetResult(
|
||||||
|
PackerOperationStatus status,
|
||||||
|
String summary,
|
||||||
|
AssetReference assetReference,
|
||||||
|
Path assetRoot,
|
||||||
|
Path manifestPath) {
|
||||||
|
|
||||||
|
public RegisterAssetResult {
|
||||||
|
Objects.requireNonNull(status, "status");
|
||||||
|
summary = Objects.requireNonNull(summary, "summary").trim();
|
||||||
|
if (summary.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("summary must not be blank");
|
||||||
|
}
|
||||||
|
if (assetRoot != null) {
|
||||||
|
assetRoot = assetRoot.toAbsolutePath().normalize();
|
||||||
|
}
|
||||||
|
if (manifestPath != null) {
|
||||||
|
manifestPath = manifestPath.toAbsolutePath().normalize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package p.packer.messages.assets;
|
||||||
|
|
||||||
|
public enum AssetAction {
|
||||||
|
REGISTER
|
||||||
|
}
|
||||||
@ -26,11 +26,17 @@ public final class Packer implements Closeable {
|
|||||||
final PackerAssetDeclarationParser declarationParser = new PackerAssetDeclarationParser();
|
final PackerAssetDeclarationParser declarationParser = new PackerAssetDeclarationParser();
|
||||||
final PackerRuntimeRegistry runtimeRegistry = new PackerRuntimeRegistry(
|
final PackerRuntimeRegistry runtimeRegistry = new PackerRuntimeRegistry(
|
||||||
new PackerRuntimeLoader(workspaceFoundation, declarationParser));
|
new PackerRuntimeLoader(workspaceFoundation, declarationParser));
|
||||||
final PackerAssetDetailsService assetDetailsService = new PackerAssetDetailsService(runtimeRegistry);
|
final PackerAssetReferenceResolver assetReferenceResolver = new PackerAssetReferenceResolver(workspaceFoundation.lookup());
|
||||||
|
final PackerAssetDetailsService assetDetailsService = new PackerAssetDetailsService(runtimeRegistry, assetReferenceResolver);
|
||||||
|
final PackerAssetActionReadService assetActionReadService = new PackerAssetActionReadService(
|
||||||
|
runtimeRegistry,
|
||||||
|
assetReferenceResolver,
|
||||||
|
workspaceFoundation.lookup());
|
||||||
final PackerProjectWriteCoordinator writeCoordinator = new PackerProjectWriteCoordinator();
|
final PackerProjectWriteCoordinator writeCoordinator = new PackerProjectWriteCoordinator();
|
||||||
return new Packer(new FileSystemPackerWorkspaceService(
|
return new Packer(new FileSystemPackerWorkspaceService(
|
||||||
workspaceFoundation,
|
workspaceFoundation,
|
||||||
assetDetailsService,
|
assetDetailsService,
|
||||||
|
assetActionReadService,
|
||||||
runtimeRegistry,
|
runtimeRegistry,
|
||||||
writeCoordinator,
|
writeCoordinator,
|
||||||
resolvedEventSink), runtimeRegistry, writeCoordinator);
|
resolvedEventSink), runtimeRegistry, writeCoordinator);
|
||||||
|
|||||||
@ -0,0 +1,20 @@
|
|||||||
|
package p.packer.models;
|
||||||
|
|
||||||
|
import p.packer.messages.assets.AssetAction;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record PackerAssetActionAvailability(
|
||||||
|
AssetAction action,
|
||||||
|
boolean enabled,
|
||||||
|
boolean visible,
|
||||||
|
String reason) {
|
||||||
|
|
||||||
|
public PackerAssetActionAvailability {
|
||||||
|
Objects.requireNonNull(action, "action");
|
||||||
|
reason = reason == null ? null : reason.trim();
|
||||||
|
if (reason != null && reason.isBlank()) {
|
||||||
|
reason = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package p.packer.models;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record PackerRegisterAssetEvaluation(
|
||||||
|
PackerResolvedAssetReference resolved,
|
||||||
|
List<PackerDiagnostic> diagnostics,
|
||||||
|
boolean canRegister,
|
||||||
|
String reason) {
|
||||||
|
|
||||||
|
public PackerRegisterAssetEvaluation {
|
||||||
|
Objects.requireNonNull(resolved, "resolved");
|
||||||
|
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
|
||||||
|
reason = reason == null ? null : reason.trim();
|
||||||
|
if (reason != null && reason.isBlank()) {
|
||||||
|
reason = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package p.packer.models;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public record PackerResolvedAssetReference(
|
||||||
|
Path assetRoot,
|
||||||
|
Optional<PackerRegistryEntry> registryEntry,
|
||||||
|
Optional<PackerRuntimeAsset> runtimeAsset,
|
||||||
|
List<PackerDiagnostic> diagnostics) {
|
||||||
|
|
||||||
|
public PackerResolvedAssetReference {
|
||||||
|
assetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
|
||||||
|
registryEntry = Objects.requireNonNull(registryEntry, "registryEntry");
|
||||||
|
runtimeAsset = Objects.requireNonNull(runtimeAsset, "runtimeAsset");
|
||||||
|
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,6 +5,8 @@ import p.packer.messages.PackerOperationStatus;
|
|||||||
import p.packer.messages.PackerProjectContext;
|
import p.packer.messages.PackerProjectContext;
|
||||||
import p.packer.messages.assets.AssetFamilyCatalog;
|
import p.packer.messages.assets.AssetFamilyCatalog;
|
||||||
import p.packer.messages.AssetReference;
|
import p.packer.messages.AssetReference;
|
||||||
|
import p.packer.messages.GetAssetActionsRequest;
|
||||||
|
import p.packer.messages.GetAssetActionsResult;
|
||||||
import p.packer.messages.assets.OutputFormatCatalog;
|
import p.packer.messages.assets.OutputFormatCatalog;
|
||||||
import p.packer.messages.assets.PackerAssetState;
|
import p.packer.messages.assets.PackerAssetState;
|
||||||
import p.packer.messages.assets.PackerBuildParticipation;
|
import p.packer.messages.assets.PackerBuildParticipation;
|
||||||
@ -21,9 +23,12 @@ import p.packer.messages.InitWorkspaceRequest;
|
|||||||
import p.packer.messages.InitWorkspaceResult;
|
import p.packer.messages.InitWorkspaceResult;
|
||||||
import p.packer.messages.ListAssetsRequest;
|
import p.packer.messages.ListAssetsRequest;
|
||||||
import p.packer.messages.ListAssetsResult;
|
import p.packer.messages.ListAssetsResult;
|
||||||
|
import p.packer.messages.RegisterAssetRequest;
|
||||||
|
import p.packer.messages.RegisterAssetResult;
|
||||||
import p.packer.PackerWorkspaceService;
|
import p.packer.PackerWorkspaceService;
|
||||||
import p.packer.models.PackerAssetDeclarationParseResult;
|
import p.packer.models.PackerAssetDeclarationParseResult;
|
||||||
import p.packer.models.PackerAssetIdentity;
|
import p.packer.models.PackerAssetIdentity;
|
||||||
|
import p.packer.models.PackerRegisterAssetEvaluation;
|
||||||
import p.packer.models.PackerAssetSummary;
|
import p.packer.models.PackerAssetSummary;
|
||||||
import p.packer.models.PackerDiagnostic;
|
import p.packer.models.PackerDiagnostic;
|
||||||
import p.packer.models.PackerRegistryEntry;
|
import p.packer.models.PackerRegistryEntry;
|
||||||
@ -41,6 +46,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
|||||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||||
private final PackerWorkspaceFoundation workspaceFoundation;
|
private final PackerWorkspaceFoundation workspaceFoundation;
|
||||||
private final PackerAssetDetailsService detailsService;
|
private final PackerAssetDetailsService detailsService;
|
||||||
|
private final PackerAssetActionReadService actionReadService;
|
||||||
private final PackerRuntimeRegistry runtimeRegistry;
|
private final PackerRuntimeRegistry runtimeRegistry;
|
||||||
private final PackerProjectWriteCoordinator writeCoordinator;
|
private final PackerProjectWriteCoordinator writeCoordinator;
|
||||||
private final PackerEventSink eventSink;
|
private final PackerEventSink eventSink;
|
||||||
@ -48,11 +54,13 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
|||||||
public FileSystemPackerWorkspaceService(
|
public FileSystemPackerWorkspaceService(
|
||||||
PackerWorkspaceFoundation workspaceFoundation,
|
PackerWorkspaceFoundation workspaceFoundation,
|
||||||
PackerAssetDetailsService detailsService,
|
PackerAssetDetailsService detailsService,
|
||||||
|
PackerAssetActionReadService actionReadService,
|
||||||
PackerRuntimeRegistry runtimeRegistry,
|
PackerRuntimeRegistry runtimeRegistry,
|
||||||
PackerProjectWriteCoordinator writeCoordinator,
|
PackerProjectWriteCoordinator writeCoordinator,
|
||||||
PackerEventSink eventSink) {
|
PackerEventSink eventSink) {
|
||||||
this.workspaceFoundation = Objects.requireNonNull(workspaceFoundation, "workspaceFoundation");
|
this.workspaceFoundation = Objects.requireNonNull(workspaceFoundation, "workspaceFoundation");
|
||||||
this.detailsService = Objects.requireNonNull(detailsService, "detailsService");
|
this.detailsService = Objects.requireNonNull(detailsService, "detailsService");
|
||||||
|
this.actionReadService = Objects.requireNonNull(actionReadService, "actionReadService");
|
||||||
this.runtimeRegistry = Objects.requireNonNull(runtimeRegistry, "runtimeRegistry");
|
this.runtimeRegistry = Objects.requireNonNull(runtimeRegistry, "runtimeRegistry");
|
||||||
this.writeCoordinator = Objects.requireNonNull(writeCoordinator, "writeCoordinator");
|
this.writeCoordinator = Objects.requireNonNull(writeCoordinator, "writeCoordinator");
|
||||||
this.eventSink = Objects.requireNonNull(eventSink, "eventSink");
|
this.eventSink = Objects.requireNonNull(eventSink, "eventSink");
|
||||||
@ -133,6 +141,11 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
|||||||
return detailsService.getAssetDetails(request);
|
return detailsService.getAssetDetails(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GetAssetActionsResult getAssetActions(GetAssetActionsRequest request) {
|
||||||
|
return actionReadService.getAssetActions(request);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CreateAssetResult createAsset(CreateAssetRequest request) {
|
public CreateAssetResult createAsset(CreateAssetRequest request) {
|
||||||
final CreateAssetRequest safeRequest = Objects.requireNonNull(request, "request");
|
final CreateAssetRequest safeRequest = Objects.requireNonNull(request, "request");
|
||||||
@ -142,6 +155,14 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
|||||||
return writeCoordinator.execute(project, () -> createAssetInWriteLane(safeRequest, events));
|
return writeCoordinator.execute(project, () -> createAssetInWriteLane(safeRequest, events));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RegisterAssetResult registerAsset(RegisterAssetRequest request) {
|
||||||
|
final RegisterAssetRequest safeRequest = Objects.requireNonNull(request, "request");
|
||||||
|
final PackerProjectContext project = safeRequest.project();
|
||||||
|
final PackerOperationEventEmitter events = new PackerOperationEventEmitter(project, eventSink);
|
||||||
|
return writeCoordinator.execute(project, () -> registerAssetInWriteLane(safeRequest, events));
|
||||||
|
}
|
||||||
|
|
||||||
private CreateAssetResult createAssetInWriteLane(
|
private CreateAssetResult createAssetInWriteLane(
|
||||||
CreateAssetRequest request,
|
CreateAssetRequest request,
|
||||||
PackerOperationEventEmitter events) {
|
PackerOperationEventEmitter events) {
|
||||||
@ -216,6 +237,50 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private RegisterAssetResult registerAssetInWriteLane(
|
||||||
|
RegisterAssetRequest request,
|
||||||
|
PackerOperationEventEmitter events) {
|
||||||
|
final PackerProjectContext project = request.project();
|
||||||
|
workspaceFoundation.initWorkspace(new InitWorkspaceRequest(project));
|
||||||
|
final PackerRuntimeSnapshot snapshot = runtimeRegistry.getOrLoad(project).snapshot();
|
||||||
|
final PackerRegisterAssetEvaluation evaluation = actionReadService.evaluateRegister(snapshot, project, request.assetReference());
|
||||||
|
final Path assetRoot = evaluation.resolved().assetRoot();
|
||||||
|
final Path manifestPath = assetRoot.resolve("asset.json");
|
||||||
|
final String relativeAssetRoot = relativeAssetRoot(project, assetRoot);
|
||||||
|
if (!evaluation.canRegister()) {
|
||||||
|
final String summary = Objects.requireNonNullElse(evaluation.reason(), "Asset cannot be registered.");
|
||||||
|
return registerFailureResult(events, summary, assetRoot, manifestPath, List.of(relativeAssetRoot));
|
||||||
|
}
|
||||||
|
|
||||||
|
final var declaration = evaluation.resolved().runtimeAsset().orElseThrow().parsedDeclaration().declaration();
|
||||||
|
try {
|
||||||
|
final PackerRegistryState registry = workspaceFoundation.loadRegistry(project);
|
||||||
|
final PackerRegistryEntry entry = workspaceFoundation.registerExistingAsset(
|
||||||
|
project,
|
||||||
|
registry,
|
||||||
|
assetRoot,
|
||||||
|
declaration.assetUuid());
|
||||||
|
final PackerRegistryState updated = workspaceFoundation.appendAllocatedEntry(registry, entry);
|
||||||
|
workspaceFoundation.saveRegistry(project, updated);
|
||||||
|
runtimeRegistry.refresh(project);
|
||||||
|
final RegisterAssetResult result = new RegisterAssetResult(
|
||||||
|
PackerOperationStatus.SUCCESS,
|
||||||
|
"Asset registered: " + relativeAssetRoot,
|
||||||
|
AssetReference.forAssetId(entry.assetId()),
|
||||||
|
assetRoot,
|
||||||
|
manifestPath);
|
||||||
|
events.emit(PackerEventKind.ACTION_APPLIED, result.summary(), List.of(relativeAssetRoot));
|
||||||
|
return result;
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
return registerFailureResult(
|
||||||
|
events,
|
||||||
|
"Unable to register asset: " + exception.getMessage(),
|
||||||
|
assetRoot,
|
||||||
|
manifestPath,
|
||||||
|
List.of(relativeAssetRoot));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private CreateAssetResult failureResult(
|
private CreateAssetResult failureResult(
|
||||||
PackerOperationEventEmitter events,
|
PackerOperationEventEmitter events,
|
||||||
String summary,
|
String summary,
|
||||||
@ -232,6 +297,22 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private RegisterAssetResult registerFailureResult(
|
||||||
|
PackerOperationEventEmitter events,
|
||||||
|
String summary,
|
||||||
|
Path assetRoot,
|
||||||
|
Path manifestPath,
|
||||||
|
List<String> affectedAssets) {
|
||||||
|
final RegisterAssetResult result = new RegisterAssetResult(
|
||||||
|
PackerOperationStatus.FAILED,
|
||||||
|
summary,
|
||||||
|
null,
|
||||||
|
assetRoot,
|
||||||
|
manifestPath);
|
||||||
|
events.emit(PackerEventKind.ACTION_FAILED, result.summary(), affectedAssets);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private void writeManifest(Path manifestPath, CreateAssetRequest request, String assetUuid) throws IOException {
|
private void writeManifest(Path manifestPath, CreateAssetRequest request, String assetUuid) throws IOException {
|
||||||
final Map<String, Object> manifest = new LinkedHashMap<>();
|
final Map<String, Object> manifest = new LinkedHashMap<>();
|
||||||
manifest.put("schema_version", 1);
|
manifest.put("schema_version", 1);
|
||||||
@ -321,4 +402,8 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
|||||||
.toString()
|
.toString()
|
||||||
.replace('\\', '/'));
|
.replace('\\', '/'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String relativeAssetRoot(PackerProjectContext project, Path assetRoot) {
|
||||||
|
return PackerWorkspacePaths.relativeAssetRoot(project, assetRoot).replace('\\', '/');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,131 @@
|
|||||||
|
package p.packer.services;
|
||||||
|
|
||||||
|
import p.packer.dtos.PackerAssetActionAvailabilityDTO;
|
||||||
|
import p.packer.messages.AssetReference;
|
||||||
|
import p.packer.messages.GetAssetActionsRequest;
|
||||||
|
import p.packer.messages.GetAssetActionsResult;
|
||||||
|
import p.packer.messages.PackerOperationStatus;
|
||||||
|
import p.packer.messages.PackerProjectContext;
|
||||||
|
import p.packer.messages.assets.AssetAction;
|
||||||
|
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
|
||||||
|
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
|
||||||
|
import p.packer.models.PackerAssetActionAvailability;
|
||||||
|
import p.packer.models.PackerAssetDeclaration;
|
||||||
|
import p.packer.models.PackerAssetDeclarationParseResult;
|
||||||
|
import p.packer.models.PackerDiagnostic;
|
||||||
|
import p.packer.models.PackerRegisterAssetEvaluation;
|
||||||
|
import p.packer.models.PackerRegistryEntry;
|
||||||
|
import p.packer.models.PackerResolvedAssetReference;
|
||||||
|
import p.packer.models.PackerRuntimeAsset;
|
||||||
|
import p.packer.models.PackerRuntimeSnapshot;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public final class PackerAssetActionReadService {
|
||||||
|
private final PackerRuntimeRegistry runtimeRegistry;
|
||||||
|
private final PackerAssetReferenceResolver assetReferenceResolver;
|
||||||
|
private final PackerRegistryLookup registryLookup;
|
||||||
|
|
||||||
|
public PackerAssetActionReadService(
|
||||||
|
PackerRuntimeRegistry runtimeRegistry,
|
||||||
|
PackerAssetReferenceResolver assetReferenceResolver,
|
||||||
|
PackerRegistryLookup registryLookup) {
|
||||||
|
this.runtimeRegistry = Objects.requireNonNull(runtimeRegistry, "runtimeRegistry");
|
||||||
|
this.assetReferenceResolver = Objects.requireNonNull(assetReferenceResolver, "assetReferenceResolver");
|
||||||
|
this.registryLookup = Objects.requireNonNull(registryLookup, "registryLookup");
|
||||||
|
}
|
||||||
|
|
||||||
|
public GetAssetActionsResult getAssetActions(GetAssetActionsRequest request) {
|
||||||
|
final PackerProjectContext project = Objects.requireNonNull(request, "request").project();
|
||||||
|
final PackerRuntimeSnapshot snapshot = runtimeRegistry.getOrLoad(project).snapshot();
|
||||||
|
final PackerRegisterAssetEvaluation registerEvaluation = evaluateRegister(snapshot, project, request.assetReference());
|
||||||
|
final List<PackerAssetActionAvailability> actions = new ArrayList<>();
|
||||||
|
if (registerEvaluation.resolved().registryEntry().isEmpty()) {
|
||||||
|
actions.add(new PackerAssetActionAvailability(
|
||||||
|
AssetAction.REGISTER,
|
||||||
|
registerEvaluation.canRegister(),
|
||||||
|
true,
|
||||||
|
registerEvaluation.reason()));
|
||||||
|
}
|
||||||
|
|
||||||
|
final PackerOperationStatus status;
|
||||||
|
if (registerEvaluation.resolved().runtimeAsset().isEmpty()) {
|
||||||
|
status = PackerOperationStatus.FAILED;
|
||||||
|
} else if (registerEvaluation.diagnostics().stream().anyMatch(PackerDiagnostic::blocking)) {
|
||||||
|
status = PackerOperationStatus.PARTIAL;
|
||||||
|
} else {
|
||||||
|
status = PackerOperationStatus.SUCCESS;
|
||||||
|
}
|
||||||
|
return new GetAssetActionsResult(
|
||||||
|
status,
|
||||||
|
"Asset action capabilities resolved from runtime snapshot.",
|
||||||
|
PackerReadMessageMapper.toAssetActionAvailabilityDTOs(actions),
|
||||||
|
PackerReadMessageMapper.toDiagnosticDTOs(registerEvaluation.diagnostics()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public PackerRegisterAssetEvaluation evaluateRegister(
|
||||||
|
PackerRuntimeSnapshot snapshot,
|
||||||
|
PackerProjectContext project,
|
||||||
|
AssetReference assetReference) {
|
||||||
|
final PackerResolvedAssetReference resolved = assetReferenceResolver.resolve(project, snapshot, assetReference);
|
||||||
|
final List<PackerDiagnostic> diagnostics = new ArrayList<>(resolved.diagnostics());
|
||||||
|
if (resolved.registryEntry().isPresent()) {
|
||||||
|
return new PackerRegisterAssetEvaluation(resolved, diagnostics, false, "Asset is already registered.");
|
||||||
|
}
|
||||||
|
if (resolved.runtimeAsset().isEmpty()) {
|
||||||
|
diagnostics.add(new PackerDiagnostic(
|
||||||
|
PackerDiagnosticSeverity.ERROR,
|
||||||
|
PackerDiagnosticCategory.STRUCTURAL,
|
||||||
|
"asset.json was not found for the requested asset root.",
|
||||||
|
resolved.assetRoot().resolve("asset.json"),
|
||||||
|
true));
|
||||||
|
return new PackerRegisterAssetEvaluation(resolved, diagnostics, false, "asset.json was not found for the requested asset root.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final PackerRuntimeAsset runtimeAsset = resolved.runtimeAsset().get();
|
||||||
|
final PackerAssetDeclarationParseResult parsed = runtimeAsset.parsedDeclaration();
|
||||||
|
diagnostics.addAll(parsed.diagnostics());
|
||||||
|
if (!parsed.valid()) {
|
||||||
|
return new PackerRegisterAssetEvaluation(resolved, diagnostics, false, firstBlockingReason(diagnostics, "Asset declaration is invalid."));
|
||||||
|
}
|
||||||
|
|
||||||
|
final PackerAssetDeclaration declaration = parsed.declaration();
|
||||||
|
final Optional<PackerRegistryEntry> sameUuidEntry = registryLookup.findByAssetUuid(snapshot.registry(), declaration.assetUuid());
|
||||||
|
if (sameUuidEntry.isPresent()) {
|
||||||
|
final Path registeredRoot = PackerWorkspacePaths.assetRoot(project, sameUuidEntry.get().root());
|
||||||
|
if (!registeredRoot.equals(resolved.assetRoot())) {
|
||||||
|
diagnostics.add(new PackerDiagnostic(
|
||||||
|
PackerDiagnosticSeverity.ERROR,
|
||||||
|
PackerDiagnosticCategory.STRUCTURAL,
|
||||||
|
"Field 'asset_uuid' is already registered to another asset root.",
|
||||||
|
runtimeAsset.manifestPath(),
|
||||||
|
true));
|
||||||
|
return new PackerRegisterAssetEvaluation(
|
||||||
|
resolved,
|
||||||
|
diagnostics,
|
||||||
|
false,
|
||||||
|
"Asset UUID is already registered by another asset.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final Optional<PackerRegistryEntry> sameRootEntry = registryLookup.findByRoot(project, snapshot.registry(), resolved.assetRoot());
|
||||||
|
if (sameRootEntry.isPresent()) {
|
||||||
|
return new PackerRegisterAssetEvaluation(resolved, diagnostics, false, "Asset root is already registered.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PackerRegisterAssetEvaluation(resolved, diagnostics, true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String firstBlockingReason(List<PackerDiagnostic> diagnostics, String fallback) {
|
||||||
|
return diagnostics.stream()
|
||||||
|
.filter(PackerDiagnostic::blocking)
|
||||||
|
.map(PackerDiagnostic::message)
|
||||||
|
.filter(message -> message != null && !message.isBlank())
|
||||||
|
.findFirst()
|
||||||
|
.orElse(fallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,6 +18,7 @@ import p.packer.models.PackerAssetIdentity;
|
|||||||
import p.packer.models.PackerAssetSummary;
|
import p.packer.models.PackerAssetSummary;
|
||||||
import p.packer.models.PackerDiagnostic;
|
import p.packer.models.PackerDiagnostic;
|
||||||
import p.packer.models.PackerRegistryEntry;
|
import p.packer.models.PackerRegistryEntry;
|
||||||
|
import p.packer.models.PackerResolvedAssetReference;
|
||||||
import p.packer.models.PackerRuntimeAsset;
|
import p.packer.models.PackerRuntimeAsset;
|
||||||
import p.packer.models.PackerRuntimeSnapshot;
|
import p.packer.models.PackerRuntimeSnapshot;
|
||||||
|
|
||||||
@ -31,15 +32,19 @@ import java.util.Optional;
|
|||||||
|
|
||||||
public final class PackerAssetDetailsService {
|
public final class PackerAssetDetailsService {
|
||||||
private final PackerRuntimeRegistry runtimeRegistry;
|
private final PackerRuntimeRegistry runtimeRegistry;
|
||||||
|
private final PackerAssetReferenceResolver assetReferenceResolver;
|
||||||
|
|
||||||
public PackerAssetDetailsService(PackerRuntimeRegistry runtimeRegistry) {
|
public PackerAssetDetailsService(
|
||||||
|
PackerRuntimeRegistry runtimeRegistry,
|
||||||
|
PackerAssetReferenceResolver assetReferenceResolver) {
|
||||||
this.runtimeRegistry = Objects.requireNonNull(runtimeRegistry, "runtimeRegistry");
|
this.runtimeRegistry = Objects.requireNonNull(runtimeRegistry, "runtimeRegistry");
|
||||||
|
this.assetReferenceResolver = Objects.requireNonNull(assetReferenceResolver, "assetReferenceResolver");
|
||||||
}
|
}
|
||||||
|
|
||||||
public GetAssetDetailsResult getAssetDetails(GetAssetDetailsRequest request) {
|
public GetAssetDetailsResult getAssetDetails(GetAssetDetailsRequest request) {
|
||||||
final PackerProjectContext project = Objects.requireNonNull(request, "request").project();
|
final PackerProjectContext project = Objects.requireNonNull(request, "request").project();
|
||||||
final PackerRuntimeSnapshot snapshot = runtimeRegistry.getOrLoad(project).snapshot();
|
final PackerRuntimeSnapshot snapshot = runtimeRegistry.getOrLoad(project).snapshot();
|
||||||
final ResolvedAssetReference resolved = resolveReference(project, snapshot, request.assetReference());
|
final PackerResolvedAssetReference resolved = assetReferenceResolver.resolve(project, snapshot, request.assetReference());
|
||||||
final List<PackerDiagnostic> diagnostics = new ArrayList<>(resolved.diagnostics());
|
final List<PackerDiagnostic> diagnostics = new ArrayList<>(resolved.diagnostics());
|
||||||
|
|
||||||
if (resolved.runtimeAsset().isEmpty()) {
|
if (resolved.runtimeAsset().isEmpty()) {
|
||||||
@ -100,7 +105,7 @@ public final class PackerAssetDetailsService {
|
|||||||
private GetAssetDetailsResult failureResult(
|
private GetAssetDetailsResult failureResult(
|
||||||
PackerProjectContext project,
|
PackerProjectContext project,
|
||||||
AssetReference requestedReference,
|
AssetReference requestedReference,
|
||||||
ResolvedAssetReference resolved,
|
PackerResolvedAssetReference resolved,
|
||||||
List<PackerDiagnostic> diagnostics) {
|
List<PackerDiagnostic> diagnostics) {
|
||||||
final PackerAssetState state = resolved.registryEntry().isPresent()
|
final PackerAssetState state = resolved.registryEntry().isPresent()
|
||||||
? PackerAssetState.REGISTERED
|
? PackerAssetState.REGISTERED
|
||||||
@ -157,44 +162,6 @@ public final class PackerAssetDetailsService {
|
|||||||
true));
|
true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private ResolvedAssetReference resolveReference(PackerProjectContext project, PackerRuntimeSnapshot snapshot, AssetReference assetReference) {
|
|
||||||
final PackerRegistryLookup lookup = new PackerRegistryLookup();
|
|
||||||
final String reference = Objects.requireNonNull(assetReference, "assetReference").rawValue();
|
|
||||||
final Optional<PackerRegistryEntry> byId = parseAssetId(reference).flatMap(assetId -> lookup.findByAssetId(snapshot.registry(), assetId));
|
|
||||||
if (byId.isPresent()) {
|
|
||||||
final Path assetRoot = PackerWorkspacePaths.assetRoot(project, byId.get().root());
|
|
||||||
return new ResolvedAssetReference(assetRoot, byId, findRuntimeAsset(snapshot, assetRoot), List.of());
|
|
||||||
}
|
|
||||||
|
|
||||||
final Optional<PackerRegistryEntry> byUuid = lookup.findByAssetUuid(snapshot.registry(), reference);
|
|
||||||
if (byUuid.isPresent()) {
|
|
||||||
final Path assetRoot = PackerWorkspacePaths.assetRoot(project, byUuid.get().root());
|
|
||||||
return new ResolvedAssetReference(assetRoot, byUuid, findRuntimeAsset(snapshot, assetRoot), List.of());
|
|
||||||
}
|
|
||||||
|
|
||||||
final Path candidateRoot = PackerWorkspacePaths.assetRoot(project, reference);
|
|
||||||
final Optional<PackerRuntimeAsset> runtimeAsset = findRuntimeAsset(snapshot, candidateRoot);
|
|
||||||
if (runtimeAsset.isPresent()) {
|
|
||||||
return new ResolvedAssetReference(candidateRoot, lookup.findByRoot(project, snapshot.registry(), candidateRoot), runtimeAsset, List.of());
|
|
||||||
}
|
|
||||||
|
|
||||||
final Optional<PackerRegistryEntry> registryEntry = lookup.findByRoot(project, snapshot.registry(), candidateRoot);
|
|
||||||
if (registryEntry.isPresent()) {
|
|
||||||
return new ResolvedAssetReference(candidateRoot, registryEntry, Optional.empty(), List.of());
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ResolvedAssetReference(
|
|
||||||
candidateRoot,
|
|
||||||
Optional.empty(),
|
|
||||||
Optional.empty(),
|
|
||||||
List.of(new PackerDiagnostic(
|
|
||||||
PackerDiagnosticSeverity.ERROR,
|
|
||||||
PackerDiagnosticCategory.STRUCTURAL,
|
|
||||||
"Requested asset reference could not be resolved.",
|
|
||||||
candidateRoot,
|
|
||||||
true)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private AssetReference canonicalReference(
|
private AssetReference canonicalReference(
|
||||||
PackerProjectContext project,
|
PackerProjectContext project,
|
||||||
Path assetRoot,
|
Path assetRoot,
|
||||||
@ -211,39 +178,10 @@ public final class PackerAssetDetailsService {
|
|||||||
private AssetReference canonicalReferenceOrRequested(
|
private AssetReference canonicalReferenceOrRequested(
|
||||||
PackerProjectContext project,
|
PackerProjectContext project,
|
||||||
AssetReference requestedReference,
|
AssetReference requestedReference,
|
||||||
ResolvedAssetReference resolved) {
|
PackerResolvedAssetReference resolved) {
|
||||||
if (resolved.registryEntry().isPresent() || resolved.runtimeAsset().isPresent()) {
|
if (resolved.registryEntry().isPresent() || resolved.runtimeAsset().isPresent()) {
|
||||||
return canonicalReference(project, resolved.assetRoot(), resolved.registryEntry());
|
return canonicalReference(project, resolved.assetRoot(), resolved.registryEntry());
|
||||||
}
|
}
|
||||||
return requestedReference;
|
return requestedReference;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<Integer> parseAssetId(String reference) {
|
|
||||||
try {
|
|
||||||
return Optional.of(Integer.parseInt(reference.trim()));
|
|
||||||
} catch (NumberFormatException ignored) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<PackerRuntimeAsset> findRuntimeAsset(PackerRuntimeSnapshot snapshot, Path assetRoot) {
|
|
||||||
final Path normalizedRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
|
|
||||||
return snapshot.assets().stream()
|
|
||||||
.filter(candidate -> candidate.assetRoot().equals(normalizedRoot))
|
|
||||||
.findFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
private record ResolvedAssetReference(
|
|
||||||
Path assetRoot,
|
|
||||||
Optional<PackerRegistryEntry> registryEntry,
|
|
||||||
Optional<PackerRuntimeAsset> runtimeAsset,
|
|
||||||
List<PackerDiagnostic> diagnostics) {
|
|
||||||
|
|
||||||
private ResolvedAssetReference {
|
|
||||||
assetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
|
|
||||||
registryEntry = Objects.requireNonNull(registryEntry, "registryEntry");
|
|
||||||
runtimeAsset = Objects.requireNonNull(runtimeAsset, "runtimeAsset");
|
|
||||||
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,88 @@
|
|||||||
|
package p.packer.services;
|
||||||
|
|
||||||
|
import p.packer.messages.AssetReference;
|
||||||
|
import p.packer.messages.PackerProjectContext;
|
||||||
|
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
|
||||||
|
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
|
||||||
|
import p.packer.models.PackerDiagnostic;
|
||||||
|
import p.packer.models.PackerRegistryEntry;
|
||||||
|
import p.packer.models.PackerResolvedAssetReference;
|
||||||
|
import p.packer.models.PackerRuntimeAsset;
|
||||||
|
import p.packer.models.PackerRuntimeSnapshot;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public final class PackerAssetReferenceResolver {
|
||||||
|
private final PackerRegistryLookup lookup;
|
||||||
|
|
||||||
|
public PackerAssetReferenceResolver() {
|
||||||
|
this(new PackerRegistryLookup());
|
||||||
|
}
|
||||||
|
|
||||||
|
public PackerAssetReferenceResolver(PackerRegistryLookup lookup) {
|
||||||
|
this.lookup = Objects.requireNonNull(lookup, "lookup");
|
||||||
|
}
|
||||||
|
|
||||||
|
public PackerResolvedAssetReference resolve(
|
||||||
|
PackerProjectContext project,
|
||||||
|
PackerRuntimeSnapshot snapshot,
|
||||||
|
AssetReference assetReference) {
|
||||||
|
final String reference = Objects.requireNonNull(assetReference, "assetReference").rawValue();
|
||||||
|
final Optional<PackerRegistryEntry> byId = parseAssetId(reference)
|
||||||
|
.flatMap(assetId -> lookup.findByAssetId(snapshot.registry(), assetId));
|
||||||
|
if (byId.isPresent()) {
|
||||||
|
final Path assetRoot = PackerWorkspacePaths.assetRoot(project, byId.get().root());
|
||||||
|
return new PackerResolvedAssetReference(assetRoot, byId, findRuntimeAsset(snapshot, assetRoot), List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
final Optional<PackerRegistryEntry> byUuid = lookup.findByAssetUuid(snapshot.registry(), reference);
|
||||||
|
if (byUuid.isPresent()) {
|
||||||
|
final Path assetRoot = PackerWorkspacePaths.assetRoot(project, byUuid.get().root());
|
||||||
|
return new PackerResolvedAssetReference(assetRoot, byUuid, findRuntimeAsset(snapshot, assetRoot), List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
final Path candidateRoot = PackerWorkspacePaths.assetRoot(project, reference);
|
||||||
|
final Optional<PackerRuntimeAsset> runtimeAsset = findRuntimeAsset(snapshot, candidateRoot);
|
||||||
|
if (runtimeAsset.isPresent()) {
|
||||||
|
return new PackerResolvedAssetReference(
|
||||||
|
candidateRoot,
|
||||||
|
lookup.findByRoot(project, snapshot.registry(), candidateRoot),
|
||||||
|
runtimeAsset,
|
||||||
|
List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
final Optional<PackerRegistryEntry> registryEntry = lookup.findByRoot(project, snapshot.registry(), candidateRoot);
|
||||||
|
if (registryEntry.isPresent()) {
|
||||||
|
return new PackerResolvedAssetReference(candidateRoot, registryEntry, Optional.empty(), List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PackerResolvedAssetReference(
|
||||||
|
candidateRoot,
|
||||||
|
Optional.empty(),
|
||||||
|
Optional.empty(),
|
||||||
|
List.of(new PackerDiagnostic(
|
||||||
|
PackerDiagnosticSeverity.ERROR,
|
||||||
|
PackerDiagnosticCategory.STRUCTURAL,
|
||||||
|
"Requested asset reference could not be resolved.",
|
||||||
|
candidateRoot,
|
||||||
|
true)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Integer> parseAssetId(String reference) {
|
||||||
|
try {
|
||||||
|
return Optional.of(Integer.parseInt(reference.trim()));
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<PackerRuntimeAsset> findRuntimeAsset(PackerRuntimeSnapshot snapshot, Path assetRoot) {
|
||||||
|
final Path normalizedRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
|
||||||
|
return snapshot.assets().stream()
|
||||||
|
.filter(candidate -> candidate.assetRoot().equals(normalizedRoot))
|
||||||
|
.findFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -26,6 +26,30 @@ public final class PackerIdentityAllocator {
|
|||||||
return new PackerRegistryEntry(state.nextAssetId(), UUID.randomUUID().toString(), relativeRoot);
|
return new PackerRegistryEntry(state.nextAssetId(), UUID.randomUUID().toString(), relativeRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PackerRegistryEntry registerExisting(
|
||||||
|
PackerProjectContext project,
|
||||||
|
PackerRegistryState state,
|
||||||
|
Path assetRoot,
|
||||||
|
String assetUuid) {
|
||||||
|
Objects.requireNonNull(project, "project");
|
||||||
|
Objects.requireNonNull(state, "state");
|
||||||
|
final Path normalizedAssetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
|
||||||
|
final String relativeRoot = PackerWorkspacePaths.relativeAssetRoot(project, normalizedAssetRoot);
|
||||||
|
final String normalizedAssetUuid = Objects.requireNonNull(assetUuid, "assetUuid").trim();
|
||||||
|
if (normalizedAssetUuid.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("assetUuid must not be blank");
|
||||||
|
}
|
||||||
|
final boolean alreadyRegisteredRoot = state.assets().stream().anyMatch(entry -> entry.root().equals(relativeRoot));
|
||||||
|
if (alreadyRegisteredRoot) {
|
||||||
|
throw new p.packer.exceptions.PackerRegistryException("Asset root is already registered: " + relativeRoot);
|
||||||
|
}
|
||||||
|
final boolean alreadyRegisteredUuid = state.assets().stream().anyMatch(entry -> entry.assetUuid().equals(normalizedAssetUuid));
|
||||||
|
if (alreadyRegisteredUuid) {
|
||||||
|
throw new p.packer.exceptions.PackerRegistryException("Asset UUID is already registered: " + normalizedAssetUuid);
|
||||||
|
}
|
||||||
|
return new PackerRegistryEntry(state.nextAssetId(), normalizedAssetUuid, relativeRoot);
|
||||||
|
}
|
||||||
|
|
||||||
public PackerRegistryState append(PackerRegistryState state, PackerRegistryEntry entry) {
|
public PackerRegistryState append(PackerRegistryState state, PackerRegistryEntry entry) {
|
||||||
Objects.requireNonNull(state, "state");
|
Objects.requireNonNull(state, "state");
|
||||||
Objects.requireNonNull(entry, "entry");
|
Objects.requireNonNull(entry, "entry");
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
package p.packer.services;
|
package p.packer.services;
|
||||||
|
|
||||||
import p.packer.dtos.PackerAssetDetailsDTO;
|
import p.packer.dtos.PackerAssetDetailsDTO;
|
||||||
|
import p.packer.dtos.PackerAssetActionAvailabilityDTO;
|
||||||
import p.packer.dtos.PackerAssetIdentityDTO;
|
import p.packer.dtos.PackerAssetIdentityDTO;
|
||||||
import p.packer.dtos.PackerAssetSummaryDTO;
|
import p.packer.dtos.PackerAssetSummaryDTO;
|
||||||
import p.packer.dtos.PackerCodecConfigurationFieldDTO;
|
import p.packer.dtos.PackerCodecConfigurationFieldDTO;
|
||||||
import p.packer.dtos.PackerDiagnosticDTO;
|
import p.packer.dtos.PackerDiagnosticDTO;
|
||||||
import p.packer.messages.assets.OutputCodecCatalog;
|
import p.packer.messages.assets.OutputCodecCatalog;
|
||||||
|
import p.packer.models.PackerAssetActionAvailability;
|
||||||
import p.packer.models.PackerAssetDetails;
|
import p.packer.models.PackerAssetDetails;
|
||||||
import p.packer.models.PackerAssetIdentity;
|
import p.packer.models.PackerAssetIdentity;
|
||||||
import p.packer.models.PackerAssetSummary;
|
import p.packer.models.PackerAssetSummary;
|
||||||
@ -50,6 +52,11 @@ public final class PackerReadMessageMapper {
|
|||||||
return diagnostics.stream().map(PackerReadMessageMapper::toDiagnosticDTO).toList();
|
return diagnostics.stream().map(PackerReadMessageMapper::toDiagnosticDTO).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static List<PackerAssetActionAvailabilityDTO> toAssetActionAvailabilityDTOs(
|
||||||
|
List<PackerAssetActionAvailability> actions) {
|
||||||
|
return actions.stream().map(PackerReadMessageMapper::toAssetActionAvailabilityDTO).toList();
|
||||||
|
}
|
||||||
|
|
||||||
private static PackerAssetIdentityDTO toAssetIdentityDTO(PackerAssetIdentity identity) {
|
private static PackerAssetIdentityDTO toAssetIdentityDTO(PackerAssetIdentity identity) {
|
||||||
return new PackerAssetIdentityDTO(
|
return new PackerAssetIdentityDTO(
|
||||||
identity.assetId(),
|
identity.assetId(),
|
||||||
@ -85,4 +92,13 @@ public final class PackerReadMessageMapper {
|
|||||||
diagnostic.evidencePath(),
|
diagnostic.evidencePath(),
|
||||||
diagnostic.blocking());
|
diagnostic.blocking());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static PackerAssetActionAvailabilityDTO toAssetActionAvailabilityDTO(
|
||||||
|
PackerAssetActionAvailability action) {
|
||||||
|
return new PackerAssetActionAvailabilityDTO(
|
||||||
|
action.action(),
|
||||||
|
action.enabled(),
|
||||||
|
action.visible(),
|
||||||
|
action.reason());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -58,6 +58,14 @@ public final class PackerWorkspaceFoundation {
|
|||||||
return identityAllocator.allocate(project, state, assetRoot);
|
return identityAllocator.allocate(project, state, assetRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PackerRegistryEntry registerExistingAsset(
|
||||||
|
PackerProjectContext project,
|
||||||
|
PackerRegistryState state,
|
||||||
|
java.nio.file.Path assetRoot,
|
||||||
|
String assetUuid) {
|
||||||
|
return identityAllocator.registerExisting(project, state, assetRoot, assetUuid);
|
||||||
|
}
|
||||||
|
|
||||||
public PackerRegistryState appendAllocatedEntry(PackerRegistryState state, PackerRegistryEntry entry) {
|
public PackerRegistryState appendAllocatedEntry(PackerRegistryState state, PackerRegistryEntry entry) {
|
||||||
return identityAllocator.append(state, entry);
|
return identityAllocator.append(state, entry);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,8 +14,12 @@ import p.packer.events.PackerEvent;
|
|||||||
import p.packer.events.PackerEventKind;
|
import p.packer.events.PackerEventKind;
|
||||||
import p.packer.messages.CreateAssetRequest;
|
import p.packer.messages.CreateAssetRequest;
|
||||||
import p.packer.messages.CreateAssetResult;
|
import p.packer.messages.CreateAssetResult;
|
||||||
|
import p.packer.messages.GetAssetActionsRequest;
|
||||||
import p.packer.messages.GetAssetDetailsRequest;
|
import p.packer.messages.GetAssetDetailsRequest;
|
||||||
import p.packer.messages.ListAssetsRequest;
|
import p.packer.messages.ListAssetsRequest;
|
||||||
|
import p.packer.messages.RegisterAssetRequest;
|
||||||
|
import p.packer.messages.RegisterAssetResult;
|
||||||
|
import p.packer.messages.assets.AssetAction;
|
||||||
import p.packer.testing.PackerFixtureLocator;
|
import p.packer.testing.PackerFixtureLocator;
|
||||||
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@ -143,6 +147,143 @@ final class FileSystemPackerWorkspaceServiceTest {
|
|||||||
assertTrue(detailsResult.diagnostics().isEmpty());
|
assertTrue(detailsResult.diagnostics().isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void exposesRegisterActionForValidUnregisteredAsset() throws Exception {
|
||||||
|
final Path projectRoot = copyFixture("workspaces/orphan-valid", tempDir.resolve("orphan-actions"));
|
||||||
|
final FileSystemPackerWorkspaceService service = service();
|
||||||
|
|
||||||
|
final var result = service.getAssetActions(new GetAssetActionsRequest(
|
||||||
|
project(projectRoot),
|
||||||
|
AssetReference.forRelativeAssetRoot("orphans/ui_sounds")));
|
||||||
|
|
||||||
|
assertEquals(PackerOperationStatus.SUCCESS, result.status());
|
||||||
|
assertEquals(1, result.actions().size());
|
||||||
|
assertEquals(AssetAction.REGISTER, result.actions().getFirst().action());
|
||||||
|
assertTrue(result.actions().getFirst().visible());
|
||||||
|
assertTrue(result.actions().getFirst().enabled());
|
||||||
|
assertNull(result.actions().getFirst().reason());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hidesRegisterActionForRegisteredAsset() throws Exception {
|
||||||
|
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("managed-actions"));
|
||||||
|
final FileSystemPackerWorkspaceService service = service();
|
||||||
|
|
||||||
|
final var result = service.getAssetActions(new GetAssetActionsRequest(
|
||||||
|
project(projectRoot),
|
||||||
|
AssetReference.forAssetId(1)));
|
||||||
|
|
||||||
|
assertEquals(PackerOperationStatus.SUCCESS, result.status());
|
||||||
|
assertTrue(result.actions().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void registersExistingUnregisteredAssetAndRefreshesRuntimeView() throws Exception {
|
||||||
|
final Path projectRoot = copyFixture("workspaces/orphan-valid", tempDir.resolve("register-orphan"));
|
||||||
|
final List<PackerEvent> events = new CopyOnWriteArrayList<>();
|
||||||
|
final FileSystemPackerWorkspaceService service = service(events::add);
|
||||||
|
|
||||||
|
final RegisterAssetResult registerResult = service.registerAsset(new RegisterAssetRequest(
|
||||||
|
project(projectRoot),
|
||||||
|
AssetReference.forRelativeAssetRoot("orphans/ui_sounds")));
|
||||||
|
|
||||||
|
assertEquals(PackerOperationStatus.SUCCESS, registerResult.status());
|
||||||
|
assertNotNull(registerResult.assetReference());
|
||||||
|
assertTrue(registerResult.assetReference().isAssetIdReference());
|
||||||
|
|
||||||
|
final var detailsResult = service.getAssetDetails(new GetAssetDetailsRequest(
|
||||||
|
project(projectRoot),
|
||||||
|
registerResult.assetReference()));
|
||||||
|
|
||||||
|
assertEquals(PackerOperationStatus.SUCCESS, detailsResult.status());
|
||||||
|
assertEquals(PackerAssetState.REGISTERED, detailsResult.details().summary().state());
|
||||||
|
assertEquals("orphan-uuid-1", detailsResult.details().summary().identity().assetUuid());
|
||||||
|
assertTrue(events.stream().anyMatch(event -> event.kind() == PackerEventKind.ACTION_APPLIED));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void blocksRegisterActionForInvalidDeclaration() throws Exception {
|
||||||
|
final Path projectRoot = copyFixture("workspaces/invalid-missing-fields", tempDir.resolve("invalid-actions"));
|
||||||
|
final FileSystemPackerWorkspaceService service = service();
|
||||||
|
|
||||||
|
final var result = service.getAssetActions(new GetAssetActionsRequest(
|
||||||
|
project(projectRoot),
|
||||||
|
AssetReference.forRelativeAssetRoot("bad")));
|
||||||
|
|
||||||
|
assertEquals(PackerOperationStatus.PARTIAL, result.status());
|
||||||
|
assertEquals(1, result.actions().size());
|
||||||
|
assertFalse(result.actions().getFirst().enabled());
|
||||||
|
assertNotNull(result.actions().getFirst().reason());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsRegisterWhenAssetUuidIsAlreadyRegisteredElsewhere() throws Exception {
|
||||||
|
final Path projectRoot = tempDir.resolve("duplicate-uuid");
|
||||||
|
Files.createDirectories(projectRoot.resolve("assets/registered/ui_atlas"));
|
||||||
|
Files.createDirectories(projectRoot.resolve("assets/orphans/ui_clone"));
|
||||||
|
Files.createDirectories(projectRoot.resolve("assets/.prometeu"));
|
||||||
|
Files.writeString(projectRoot.resolve("assets/registered/ui_atlas/asset.json"), """
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"asset_uuid": "shared-uuid",
|
||||||
|
"name": "ui_atlas",
|
||||||
|
"type": "IMAGE/bank",
|
||||||
|
"inputs": {},
|
||||||
|
"output": {
|
||||||
|
"format": "TILES/indexed_v1",
|
||||||
|
"codec": "none"
|
||||||
|
},
|
||||||
|
"preload": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
Files.writeString(projectRoot.resolve("assets/orphans/ui_clone/asset.json"), """
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"asset_uuid": "shared-uuid",
|
||||||
|
"name": "ui_clone",
|
||||||
|
"type": "IMAGE/bank",
|
||||||
|
"inputs": {},
|
||||||
|
"output": {
|
||||||
|
"format": "TILES/indexed_v1",
|
||||||
|
"codec": "none"
|
||||||
|
},
|
||||||
|
"preload": {
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
Files.writeString(projectRoot.resolve("assets/.prometeu/index.json"), """
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"next_asset_id": 2,
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"asset_id": 1,
|
||||||
|
"asset_uuid": "shared-uuid",
|
||||||
|
"root": "registered/ui_atlas",
|
||||||
|
"included_in_build": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
final FileSystemPackerWorkspaceService service = service();
|
||||||
|
|
||||||
|
final var actions = service.getAssetActions(new GetAssetActionsRequest(
|
||||||
|
project(projectRoot),
|
||||||
|
AssetReference.forRelativeAssetRoot("orphans/ui_clone")));
|
||||||
|
assertEquals(1, actions.actions().size());
|
||||||
|
assertFalse(actions.actions().getFirst().enabled());
|
||||||
|
assertNotNull(actions.actions().getFirst().reason());
|
||||||
|
|
||||||
|
final RegisterAssetResult registerResult = service.registerAsset(new RegisterAssetRequest(
|
||||||
|
project(projectRoot),
|
||||||
|
AssetReference.forRelativeAssetRoot("orphans/ui_clone")));
|
||||||
|
assertEquals(PackerOperationStatus.FAILED, registerResult.status());
|
||||||
|
assertNull(registerResult.assetReference());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void rejectsUnsupportedFormatForSelectedFamily() {
|
void rejectsUnsupportedFormatForSelectedFamily() {
|
||||||
final Path projectRoot = tempDir.resolve("unsupported");
|
final Path projectRoot = tempDir.resolve("unsupported");
|
||||||
@ -208,9 +349,17 @@ final class FileSystemPackerWorkspaceServiceTest {
|
|||||||
final var foundation = new p.packer.services.PackerWorkspaceFoundation();
|
final var foundation = new p.packer.services.PackerWorkspaceFoundation();
|
||||||
final var parser = new p.packer.services.PackerAssetDeclarationParser();
|
final var parser = new p.packer.services.PackerAssetDeclarationParser();
|
||||||
final var runtimeRegistry = new p.packer.services.PackerRuntimeRegistry(new p.packer.services.PackerRuntimeLoader(foundation, parser));
|
final var runtimeRegistry = new p.packer.services.PackerRuntimeRegistry(new p.packer.services.PackerRuntimeLoader(foundation, parser));
|
||||||
final var detailsService = new p.packer.services.PackerAssetDetailsService(runtimeRegistry);
|
final var resolver = new p.packer.services.PackerAssetReferenceResolver(foundation.lookup());
|
||||||
|
final var detailsService = new p.packer.services.PackerAssetDetailsService(runtimeRegistry, resolver);
|
||||||
|
final var actionReadService = new p.packer.services.PackerAssetActionReadService(runtimeRegistry, resolver, foundation.lookup());
|
||||||
final var writeCoordinator = new p.packer.services.PackerProjectWriteCoordinator();
|
final var writeCoordinator = new p.packer.services.PackerProjectWriteCoordinator();
|
||||||
return new FileSystemPackerWorkspaceService(foundation, detailsService, runtimeRegistry, writeCoordinator, eventSink);
|
return new FileSystemPackerWorkspaceService(
|
||||||
|
foundation,
|
||||||
|
detailsService,
|
||||||
|
actionReadService,
|
||||||
|
runtimeRegistry,
|
||||||
|
writeCoordinator,
|
||||||
|
eventSink);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Path copyFixture(String relativePath, Path targetRoot) throws Exception {
|
private Path copyFixture(String relativePath, Path targetRoot) throws Exception {
|
||||||
|
|||||||
@ -88,7 +88,9 @@ final class PackerAssetDetailsServiceTest {
|
|||||||
private PackerAssetDetailsService service() {
|
private PackerAssetDetailsService service() {
|
||||||
final var foundation = new p.packer.services.PackerWorkspaceFoundation();
|
final var foundation = new p.packer.services.PackerWorkspaceFoundation();
|
||||||
final var parser = new PackerAssetDeclarationParser();
|
final var parser = new PackerAssetDeclarationParser();
|
||||||
return new PackerAssetDetailsService(new PackerRuntimeRegistry(new PackerRuntimeLoader(foundation, parser)));
|
final var runtimeRegistry = new PackerRuntimeRegistry(new PackerRuntimeLoader(foundation, parser));
|
||||||
|
final var resolver = new PackerAssetReferenceResolver(foundation.lookup());
|
||||||
|
return new PackerAssetDetailsService(runtimeRegistry, resolver);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Path copyFixture(String relativePath, Path targetRoot) throws Exception {
|
private Path copyFixture(String relativePath, Path targetRoot) throws Exception {
|
||||||
|
|||||||
@ -100,6 +100,7 @@ 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_EMPTY("assets.actions.empty"),
|
||||||
ASSETS_ACTION_REGISTER("assets.action.register"),
|
ASSETS_ACTION_REGISTER("assets.action.register"),
|
||||||
ASSETS_ACTION_INCLUDE_IN_BUILD("assets.action.includeInBuild"),
|
ASSETS_ACTION_INCLUDE_IN_BUILD("assets.action.includeInBuild"),
|
||||||
ASSETS_ACTION_EXCLUDE_FROM_BUILD("assets.action.excludeFromBuild"),
|
ASSETS_ACTION_EXCLUDE_FROM_BUILD("assets.action.excludeFromBuild"),
|
||||||
|
|||||||
@ -1,20 +1,28 @@
|
|||||||
package p.studio.workspaces.assets.details;
|
package p.studio.workspaces.assets.details;
|
||||||
|
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.ScrollPane;
|
import javafx.scene.control.ScrollPane;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import p.packer.messages.AssetReference;
|
import p.packer.messages.AssetReference;
|
||||||
|
import p.packer.dtos.PackerAssetActionAvailabilityDTO;
|
||||||
import p.packer.dtos.PackerAssetDetailsDTO;
|
import p.packer.dtos.PackerAssetDetailsDTO;
|
||||||
import p.packer.messages.GetAssetDetailsRequest;
|
import p.packer.messages.GetAssetDetailsRequest;
|
||||||
|
import p.packer.messages.GetAssetActionsRequest;
|
||||||
|
import p.packer.messages.RegisterAssetRequest;
|
||||||
|
import p.packer.messages.RegisterAssetResult;
|
||||||
|
import p.packer.messages.assets.AssetAction;
|
||||||
import p.studio.Container;
|
import p.studio.Container;
|
||||||
import p.studio.events.StudioWorkspaceEventBus;
|
import p.studio.events.StudioWorkspaceEventBus;
|
||||||
import p.studio.projects.ProjectReference;
|
import p.studio.projects.ProjectReference;
|
||||||
import p.studio.utilities.i18n.I18n;
|
import p.studio.utilities.i18n.I18n;
|
||||||
import p.studio.workspaces.assets.details.contract.AssetDetailsContractControl;
|
import p.studio.workspaces.assets.details.contract.AssetDetailsContractControl;
|
||||||
import p.studio.workspaces.assets.details.summary.AssetDetailsSummaryControl;
|
import p.studio.workspaces.assets.details.summary.AssetDetailsSummaryControl;
|
||||||
|
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetAction;
|
||||||
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetDetails;
|
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetDetails;
|
||||||
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsStatus;
|
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsStatus;
|
||||||
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState;
|
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState;
|
||||||
@ -46,6 +54,8 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
|||||||
|
|
||||||
private AssetWorkspaceDetailsViewState viewState = AssetDetailsViewStateFactory.empty();
|
private AssetWorkspaceDetailsViewState viewState = AssetDetailsViewStateFactory.empty();
|
||||||
private boolean readyMounted;
|
private boolean readyMounted;
|
||||||
|
private boolean actionRunning;
|
||||||
|
private String actionFeedbackMessage;
|
||||||
|
|
||||||
public AssetDetailsControl(
|
public AssetDetailsControl(
|
||||||
ProjectReference projectReference,
|
ProjectReference projectReference,
|
||||||
@ -109,12 +119,16 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
|||||||
|
|
||||||
private void loadSelection(AssetReference assetReference) {
|
private void loadSelection(AssetReference assetReference) {
|
||||||
final long generation = loadGeneration.incrementAndGet();
|
final long generation = loadGeneration.incrementAndGet();
|
||||||
|
actionRunning = false;
|
||||||
|
actionFeedbackMessage = null;
|
||||||
publishViewState(AssetDetailsViewStateFactory.loading(assetReference));
|
publishViewState(AssetDetailsViewStateFactory.loading(assetReference));
|
||||||
Container.backgroundTasks().submit(() -> loadDetails(generation, assetReference));
|
Container.backgroundTasks().submit(() -> loadDetails(generation, assetReference));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clearSelection() {
|
private void clearSelection() {
|
||||||
loadGeneration.incrementAndGet();
|
loadGeneration.incrementAndGet();
|
||||||
|
actionRunning = false;
|
||||||
|
actionFeedbackMessage = null;
|
||||||
publishViewState(AssetDetailsViewStateFactory.empty());
|
publishViewState(AssetDetailsViewStateFactory.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,12 +142,15 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
|||||||
|
|
||||||
private void loadDetails(long generation, AssetReference assetReference) {
|
private void loadDetails(long generation, AssetReference assetReference) {
|
||||||
try {
|
try {
|
||||||
final var response = Container.packer()
|
final var workspaceService = Container.packer().workspaceService();
|
||||||
.workspaceService()
|
final var response = workspaceService
|
||||||
.getAssetDetails(new GetAssetDetailsRequest(
|
.getAssetDetails(new GetAssetDetailsRequest(
|
||||||
projectReference.toPackerProjectContext(),
|
projectReference.toPackerProjectContext(),
|
||||||
assetReference));
|
assetReference));
|
||||||
final AssetWorkspaceAssetDetails details = mapDetails(response.details());
|
final var actionsResponse = workspaceService.getAssetActions(new GetAssetActionsRequest(
|
||||||
|
projectReference.toPackerProjectContext(),
|
||||||
|
assetReference));
|
||||||
|
final AssetWorkspaceAssetDetails details = mapDetails(response.details(), actionsResponse.actions());
|
||||||
Platform.runLater(() -> applyLoadedDetails(generation, assetReference, details));
|
Platform.runLater(() -> applyLoadedDetails(generation, assetReference, details));
|
||||||
} catch (RuntimeException exception) {
|
} catch (RuntimeException exception) {
|
||||||
Platform.runLater(() -> applyLoadFailure(generation, assetReference, exception));
|
Platform.runLater(() -> applyLoadFailure(generation, assetReference, exception));
|
||||||
@ -175,18 +192,21 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
|||||||
readyMounted = false;
|
readyMounted = false;
|
||||||
setDetailsTitle(null);
|
setDetailsTitle(null);
|
||||||
setWorkspaceSummary(Container.i18n().text(I18n.ASSETS_SUMMARY_EMPTY));
|
setWorkspaceSummary(Container.i18n().text(I18n.ASSETS_SUMMARY_EMPTY));
|
||||||
|
renderActions();
|
||||||
detailsContent.getChildren().setAll(AssetDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_NO_SELECTION)));
|
detailsContent.getChildren().setAll(AssetDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_NO_SELECTION)));
|
||||||
}
|
}
|
||||||
case LOADING -> {
|
case LOADING -> {
|
||||||
readyMounted = false;
|
readyMounted = false;
|
||||||
setDetailsTitle(null);
|
setDetailsTitle(null);
|
||||||
setWorkspaceSummary(Container.i18n().text(I18n.ASSETS_SUMMARY_LOADING));
|
setWorkspaceSummary(Container.i18n().text(I18n.ASSETS_SUMMARY_LOADING));
|
||||||
|
renderActions();
|
||||||
detailsContent.getChildren().setAll(AssetDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_LOADING)));
|
detailsContent.getChildren().setAll(AssetDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_LOADING)));
|
||||||
}
|
}
|
||||||
case ERROR -> {
|
case ERROR -> {
|
||||||
readyMounted = false;
|
readyMounted = false;
|
||||||
setDetailsTitle(null);
|
setDetailsTitle(null);
|
||||||
setWorkspaceSummary(Container.i18n().text(I18n.ASSETS_SUMMARY_ERROR));
|
setWorkspaceSummary(Container.i18n().text(I18n.ASSETS_SUMMARY_ERROR));
|
||||||
|
renderActions();
|
||||||
detailsContent.getChildren().setAll(AssetDetailsUiSupport.createSectionMessage(
|
detailsContent.getChildren().setAll(AssetDetailsUiSupport.createSectionMessage(
|
||||||
Objects.requireNonNullElse(viewState.detailsErrorMessage(), Container.i18n().text(I18n.ASSETS_DETAILS_EMPTY))));
|
Objects.requireNonNullElse(viewState.detailsErrorMessage(), Container.i18n().text(I18n.ASSETS_DETAILS_EMPTY))));
|
||||||
}
|
}
|
||||||
@ -195,12 +215,14 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
|||||||
readyMounted = false;
|
readyMounted = false;
|
||||||
setDetailsTitle(null);
|
setDetailsTitle(null);
|
||||||
setWorkspaceSummary(null);
|
setWorkspaceSummary(null);
|
||||||
|
renderActions();
|
||||||
detailsContent.getChildren().setAll(AssetDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_NO_SELECTION)));
|
detailsContent.getChildren().setAll(AssetDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_DETAILS_NO_SELECTION)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setDetailsTitle(viewState.selectedAssetDetails().summary().assetName());
|
setDetailsTitle(viewState.selectedAssetDetails().summary().assetName());
|
||||||
setWorkspaceSummary(null);
|
setWorkspaceSummary(null);
|
||||||
|
renderActions();
|
||||||
if (!readyMounted) {
|
if (!readyMounted) {
|
||||||
readyMounted = true;
|
readyMounted = true;
|
||||||
detailsContent.getChildren().setAll(primarySectionsRow, contractControl);
|
detailsContent.getChildren().setAll(primarySectionsRow, contractControl);
|
||||||
@ -244,9 +266,93 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
|||||||
workspaceSummaryLabel.setManaged(visible);
|
workspaceSummaryLabel.setManaged(visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
private AssetWorkspaceAssetDetails mapDetails(PackerAssetDetailsDTO details) {
|
private void renderActions() {
|
||||||
|
actionsContent.getChildren().setAll(buildActionNodes());
|
||||||
|
}
|
||||||
|
|
||||||
|
private java.util.List<Node> buildActionNodes() {
|
||||||
|
final java.util.List<Node> nodes = new java.util.ArrayList<>();
|
||||||
|
if (actionFeedbackMessage != null && !actionFeedbackMessage.isBlank()) {
|
||||||
|
nodes.add(AssetDetailsUiSupport.createSectionMessage(actionFeedbackMessage));
|
||||||
|
}
|
||||||
|
if (viewState.selectedAssetDetails() == null) {
|
||||||
|
nodes.add(AssetDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_ACTIONS_EMPTY)));
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
final var visibleActions = viewState.selectedAssetDetails().actions().stream()
|
||||||
|
.filter(AssetWorkspaceAssetAction::visible)
|
||||||
|
.toList();
|
||||||
|
if (visibleActions.isEmpty()) {
|
||||||
|
nodes.add(AssetDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_ACTIONS_EMPTY)));
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
for (AssetWorkspaceAssetAction action : visibleActions) {
|
||||||
|
final Button button = AssetDetailsUiSupport.createActionButton(AssetDetailsUiSupport.actionLabel(action.action()));
|
||||||
|
button.setDisable(actionRunning || !action.enabled());
|
||||||
|
button.setOnAction(ignored -> executeAction(action));
|
||||||
|
nodes.add(button);
|
||||||
|
if (action.reason() != null) {
|
||||||
|
nodes.add(AssetDetailsUiSupport.createSectionMessage(action.reason()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void executeAction(AssetWorkspaceAssetAction action) {
|
||||||
|
if (actionRunning || viewState.selectedAssetReference() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
actionRunning = true;
|
||||||
|
actionFeedbackMessage = null;
|
||||||
|
renderActions();
|
||||||
|
switch (action.action()) {
|
||||||
|
case REGISTER -> Container.backgroundTasks().submit(() -> registerSelectedAsset(viewState.selectedAssetReference()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerSelectedAsset(AssetReference assetReference) {
|
||||||
|
try {
|
||||||
|
final RegisterAssetResult result = Container.packer().workspaceService().registerAsset(new RegisterAssetRequest(
|
||||||
|
projectReference.toPackerProjectContext(),
|
||||||
|
assetReference));
|
||||||
|
Platform.runLater(() -> applyRegisterResult(result));
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
Platform.runLater(() -> applyRegisterFailure(exception));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyRegisterResult(RegisterAssetResult result) {
|
||||||
|
actionRunning = false;
|
||||||
|
if (result.status() == p.packer.messages.PackerOperationStatus.SUCCESS && result.assetReference() != null) {
|
||||||
|
actionFeedbackMessage = null;
|
||||||
|
workspaceBus.publish(new StudioAssetsRefreshRequestedEvent(result.assetReference()));
|
||||||
|
workspaceBus.publish(new StudioAssetsWorkspaceSelectionRequestedEvent(result.assetReference()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
actionFeedbackMessage = Objects.requireNonNullElse(result.summary(), "Unable to register asset.");
|
||||||
|
renderActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyRegisterFailure(RuntimeException exception) {
|
||||||
|
actionRunning = false;
|
||||||
|
actionFeedbackMessage = exception.getMessage() == null || exception.getMessage().isBlank()
|
||||||
|
? "Unable to register asset."
|
||||||
|
: exception.getMessage();
|
||||||
|
renderActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private AssetWorkspaceAssetDetails mapDetails(
|
||||||
|
PackerAssetDetailsDTO details,
|
||||||
|
java.util.List<PackerAssetActionAvailabilityDTO> actions) {
|
||||||
return new AssetWorkspaceAssetDetails(
|
return new AssetWorkspaceAssetDetails(
|
||||||
AssetListPackerMappings.mapSummary(details.summary()),
|
AssetListPackerMappings.mapSummary(details.summary()),
|
||||||
|
actions.stream()
|
||||||
|
.map(action -> new AssetWorkspaceAssetAction(
|
||||||
|
action.action(),
|
||||||
|
action.enabled(),
|
||||||
|
action.visible(),
|
||||||
|
action.reason()))
|
||||||
|
.toList(),
|
||||||
details.outputFormat(),
|
details.outputFormat(),
|
||||||
details.outputCodec(),
|
details.outputCodec(),
|
||||||
details.availableOutputCodecs(),
|
details.availableOutputCodecs(),
|
||||||
|
|||||||
@ -2,10 +2,12 @@ package p.studio.workspaces.assets.details;
|
|||||||
|
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
import p.packer.messages.assets.AssetAction;
|
||||||
import p.packer.messages.assets.AssetFamilyCatalog;
|
import p.packer.messages.assets.AssetFamilyCatalog;
|
||||||
import p.studio.Container;
|
import p.studio.Container;
|
||||||
import p.studio.projects.ProjectReference;
|
import p.studio.projects.ProjectReference;
|
||||||
@ -58,6 +60,13 @@ public final class AssetDetailsUiSupport {
|
|||||||
return chip;
|
return chip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Button createActionButton(String text) {
|
||||||
|
final Button button = new Button(text);
|
||||||
|
button.getStyleClass().addAll("studio-button", "studio-button-primary", "assets-details-action-button");
|
||||||
|
button.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
public static String registrationLabel(AssetWorkspaceAssetState state) {
|
public static String registrationLabel(AssetWorkspaceAssetState state) {
|
||||||
return switch (state) {
|
return switch (state) {
|
||||||
case REGISTERED -> Container.i18n().text(I18n.ASSETS_VALUE_REGISTERED);
|
case REGISTERED -> Container.i18n().text(I18n.ASSETS_VALUE_REGISTERED);
|
||||||
@ -96,6 +105,12 @@ public final class AssetDetailsUiSupport {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String actionLabel(AssetAction action) {
|
||||||
|
return switch (action) {
|
||||||
|
case REGISTER -> Container.i18n().text(I18n.ASSETS_ACTION_REGISTER);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public static String registrationChipTone(AssetWorkspaceAssetState state) {
|
public static String registrationChipTone(AssetWorkspaceAssetState state) {
|
||||||
return switch (state) {
|
return switch (state) {
|
||||||
case REGISTERED -> "assets-details-chip-registered";
|
case REGISTERED -> "assets-details-chip-registered";
|
||||||
|
|||||||
@ -0,0 +1,20 @@
|
|||||||
|
package p.studio.workspaces.assets.messages;
|
||||||
|
|
||||||
|
import p.packer.messages.assets.AssetAction;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record AssetWorkspaceAssetAction(
|
||||||
|
AssetAction action,
|
||||||
|
boolean enabled,
|
||||||
|
boolean visible,
|
||||||
|
String reason) {
|
||||||
|
|
||||||
|
public AssetWorkspaceAssetAction {
|
||||||
|
Objects.requireNonNull(action, "action");
|
||||||
|
reason = reason == null ? null : reason.trim();
|
||||||
|
if (reason != null && reason.isBlank()) {
|
||||||
|
reason = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,6 +10,7 @@ import java.util.Objects;
|
|||||||
|
|
||||||
public record AssetWorkspaceAssetDetails(
|
public record AssetWorkspaceAssetDetails(
|
||||||
AssetWorkspaceAssetSummary summary,
|
AssetWorkspaceAssetSummary summary,
|
||||||
|
List<AssetWorkspaceAssetAction> actions,
|
||||||
String outputFormat,
|
String outputFormat,
|
||||||
OutputCodecCatalog outputCodec,
|
OutputCodecCatalog outputCodec,
|
||||||
List<OutputCodecCatalog> availableOutputCodecs,
|
List<OutputCodecCatalog> availableOutputCodecs,
|
||||||
@ -18,6 +19,7 @@ public record AssetWorkspaceAssetDetails(
|
|||||||
|
|
||||||
public AssetWorkspaceAssetDetails {
|
public AssetWorkspaceAssetDetails {
|
||||||
Objects.requireNonNull(summary, "summary");
|
Objects.requireNonNull(summary, "summary");
|
||||||
|
actions = List.copyOf(Objects.requireNonNull(actions, "actions"));
|
||||||
outputFormat = Objects.requireNonNullElse(outputFormat, "unknown");
|
outputFormat = Objects.requireNonNullElse(outputFormat, "unknown");
|
||||||
outputCodec = Objects.requireNonNullElse(outputCodec, OutputCodecCatalog.UNKNOWN);
|
outputCodec = Objects.requireNonNullElse(outputCodec, OutputCodecCatalog.UNKNOWN);
|
||||||
availableOutputCodecs = List.copyOf(Objects.requireNonNull(availableOutputCodecs, "availableOutputCodecs"));
|
availableOutputCodecs = List.copyOf(Objects.requireNonNull(availableOutputCodecs, "availableOutputCodecs"));
|
||||||
|
|||||||
@ -90,6 +90,7 @@ assets.subsection.codecConfiguration=Codec Configuration
|
|||||||
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.empty=No actions available for this asset.
|
||||||
assets.action.register=Register
|
assets.action.register=Register
|
||||||
assets.action.includeInBuild=Include In Build
|
assets.action.includeInBuild=Include In Build
|
||||||
assets.action.excludeFromBuild=Exclude From Build
|
assets.action.excludeFromBuild=Exclude From Build
|
||||||
|
|||||||
@ -547,6 +547,10 @@
|
|||||||
-fx-spacing: 10;
|
-fx-spacing: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.assets-details-action-button {
|
||||||
|
-fx-max-width: Infinity;
|
||||||
|
}
|
||||||
|
|
||||||
.assets-details-section {
|
.assets-details-section {
|
||||||
-fx-background-color: #11151b;
|
-fx-background-color: #11151b;
|
||||||
-fx-background-radius: 12;
|
-fx-background-radius: 12;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user