diff --git a/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/dtos/PackerAssetDetailsDTO.java b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/dtos/PackerAssetDetailsDTO.java index 1aff06f3..55bd697b 100644 --- a/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/dtos/PackerAssetDetailsDTO.java +++ b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/dtos/PackerAssetDetailsDTO.java @@ -14,6 +14,7 @@ public record PackerAssetDetailsDTO( OutputCodecCatalog outputCodec, List availableOutputCodecs, Map> codecConfigurationFieldsByCodec, + List metadataFields, Map> inputsByRole, List diagnostics) { @@ -23,6 +24,7 @@ public record PackerAssetDetailsDTO( outputCodec = Objects.requireNonNullElse(outputCodec, OutputCodecCatalog.UNKNOWN); availableOutputCodecs = List.copyOf(Objects.requireNonNull(availableOutputCodecs, "availableOutputCodecs")); codecConfigurationFieldsByCodec = Map.copyOf(Objects.requireNonNull(codecConfigurationFieldsByCodec, "codecConfigurationFieldsByCodec")); + metadataFields = List.copyOf(Objects.requireNonNull(metadataFields, "metadataFields")); inputsByRole = Map.copyOf(Objects.requireNonNull(inputsByRole, "inputsByRole")); diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics")); } diff --git a/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/UpdateAssetContractRequest.java b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/UpdateAssetContractRequest.java index 37c9e4d4..10967b6b 100644 --- a/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/UpdateAssetContractRequest.java +++ b/prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/UpdateAssetContractRequest.java @@ -10,12 +10,14 @@ public record UpdateAssetContractRequest( AssetReference assetReference, boolean preloadEnabled, OutputCodecCatalog outputCodec, - Map codecFieldValues) { + Map codecFieldValues, + Map metadataFieldValues) { public UpdateAssetContractRequest { Objects.requireNonNull(project, "project"); Objects.requireNonNull(assetReference, "assetReference"); outputCodec = Objects.requireNonNullElse(outputCodec, OutputCodecCatalog.UNKNOWN); codecFieldValues = Map.copyOf(Objects.requireNonNullElse(codecFieldValues, Map.of())); + metadataFieldValues = Map.copyOf(Objects.requireNonNullElse(metadataFieldValues, Map.of())); } } diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerAssetDeclaration.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerAssetDeclaration.java index 07803460..95077471 100644 --- a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerAssetDeclaration.java +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerAssetDeclaration.java @@ -17,6 +17,7 @@ public record PackerAssetDeclaration( Map> inputsByRole, OutputFormatCatalog outputFormat, OutputCodecCatalog outputCodec, + Map outputMetadata, boolean preloadEnabled) { public PackerAssetDeclaration { @@ -29,6 +30,7 @@ public record PackerAssetDeclaration( inputsByRole = Map.copyOf(Objects.requireNonNull(inputsByRole, "inputsByRole")); outputFormat = Objects.requireNonNull(outputFormat, "outputFormat"); outputCodec = Objects.requireNonNull(outputCodec, "outputCodec"); + outputMetadata = Map.copyOf(Objects.requireNonNull(outputMetadata, "outputMetadata")); if (StringUtils.isBlank(assetUuid) || StringUtils.isBlank(name)) { throw new IllegalArgumentException("declaration fields must not be blank"); } diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerAssetDetails.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerAssetDetails.java index 54718d44..a52ea881 100644 --- a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerAssetDetails.java +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/PackerAssetDetails.java @@ -14,6 +14,7 @@ public record PackerAssetDetails( OutputCodecCatalog outputCodec, List availableOutputCodecs, Map> codecConfigurationFieldsByCodec, + List metadataFields, Map> inputsByRole, List diagnostics) { @@ -23,6 +24,7 @@ public record PackerAssetDetails( outputCodec = Objects.requireNonNullElse(outputCodec, OutputCodecCatalog.UNKNOWN); availableOutputCodecs = List.copyOf(Objects.requireNonNull(availableOutputCodecs, "availableOutputCodecs")); codecConfigurationFieldsByCodec = Map.copyOf(Objects.requireNonNull(codecConfigurationFieldsByCodec, "codecConfigurationFieldsByCodec")); + metadataFields = List.copyOf(Objects.requireNonNull(metadataFields, "metadataFields")); inputsByRole = Map.copyOf(Objects.requireNonNull(inputsByRole, "inputsByRole")); diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics")); } diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/FileSystemPackerWorkspaceService.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/FileSystemPackerWorkspaceService.java index 9e4f6639..d4302bfc 100644 --- a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/FileSystemPackerWorkspaceService.java +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/FileSystemPackerWorkspaceService.java @@ -672,6 +672,17 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe } codecConfigurationNode.put(normalizedFieldKey, Objects.requireNonNullElse(fieldValue, "")); }); + + final ObjectNode metadataNode = mutableObject(outputNode, "metadata"); + metadataNode.removeAll(); + request.metadataFieldValues().entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .forEach(entry -> { + if (entry.getKey() == null || entry.getKey().isBlank()) { + return; + } + metadataNode.put(entry.getKey().trim(), Objects.requireNonNullElse(entry.getValue(), "")); + }); } private ObjectNode mutableObject(ObjectNode parent, String fieldName) { diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetDeclarationParser.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetDeclarationParser.java index 71169898..697d2f96 100644 --- a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetDeclarationParser.java +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetDeclarationParser.java @@ -47,6 +47,7 @@ public final class PackerAssetDeclarationParser { final Map> inputsByRole = requiredInputs(root.path("inputs"), diagnostics, manifestPath); final OutputFormatCatalog outputFormat = requiredOutputFormat(root.path("output"), diagnostics, manifestPath); final OutputCodecCatalog outputCodec = requiredOutputCodec(root.path("output"), diagnostics, manifestPath); + final Map outputMetadata = optionalOutputMetadata(root.path("output"), diagnostics, manifestPath); final Boolean preloadEnabled = requiredBoolean(root.path("preload"), "enabled", diagnostics, manifestPath); if (schemaVersion != null && schemaVersion != SUPPORTED_SCHEMA_VERSION) { @@ -71,6 +72,7 @@ public final class PackerAssetDeclarationParser { inputsByRole, outputFormat, outputCodec, + outputMetadata, preloadEnabled), diagnostics); } @@ -156,6 +158,53 @@ public final class PackerAssetDeclarationParser { return outputCodec; } + private Map optionalOutputMetadata( + JsonNode outputNode, + List diagnostics, + Path manifestPath) { + final JsonNode metadataNode = outputNode.path("metadata"); + if (metadataNode.isMissingNode() || metadataNode.isNull()) { + return Map.of(); + } + if (!metadataNode.isObject()) { + diagnostics.add(new PackerDiagnostic( + PackerDiagnosticSeverity.ERROR, + PackerDiagnosticCategory.STRUCTURAL, + "Field 'output.metadata' must be a JSON object.", + manifestPath, + true)); + return Map.of(); + } + + final Map metadata = new LinkedHashMap<>(); + metadataNode.fields().forEachRemaining(entry -> { + final String key = Objects.requireNonNullElse(entry.getKey(), "").trim(); + if (key.isBlank()) { + diagnostics.add(new PackerDiagnostic( + PackerDiagnosticSeverity.ERROR, + PackerDiagnosticCategory.STRUCTURAL, + "Field 'output.metadata' has an invalid empty key.", + manifestPath, + true)); + return; + } + + final JsonNode valueNode = entry.getValue(); + if (valueNode == null || valueNode.isContainerNode()) { + diagnostics.add(new PackerDiagnostic( + PackerDiagnosticSeverity.ERROR, + PackerDiagnosticCategory.STRUCTURAL, + "Field 'output.metadata' values must be scalar (text/number/boolean).", + manifestPath, + true)); + return; + } + metadata.put(key, valueNode.asText()); + }); + + return Map.copyOf(metadata); + } + private Map> requiredInputs(JsonNode inputsNode, List diagnostics, Path manifestPath) { if (!inputsNode.isObject()) { diagnostics.add(missingOrInvalid("inputs", "object of input roles", manifestPath)); diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetDetailsService.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetDetailsService.java index 52a2f534..0b1ade6d 100644 --- a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetDetailsService.java +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetDetailsService.java @@ -73,6 +73,7 @@ public final class PackerAssetDetailsService { declaration.outputCodec(), outputContract.availableCodecs(), outputContract.codecConfigurationFieldsByCodec(), + metadataFields(declaration.outputMetadata()), resolveInputs(resolved.assetRoot(), declaration.inputsByRole()), diagnostics); return new GetAssetDetailsResult( @@ -111,6 +112,7 @@ public final class PackerAssetDetailsService { OutputCodecCatalog.UNKNOWN, List.of(OutputCodecCatalog.NONE), Map.of(OutputCodecCatalog.NONE, List.of()), + List.of(), Map.of(), diagnostics); return new GetAssetDetailsResult( @@ -130,6 +132,18 @@ public final class PackerAssetDetailsService { return Map.copyOf(resolved); } + private List metadataFields(Map outputMetadata) { + return outputMetadata.entrySet().stream() + .map(entry -> new PackerCodecConfigurationField( + entry.getKey(), + entry.getKey(), + PackerCodecConfigurationFieldType.TEXT, + entry.getValue(), + false, + List.of())) + .toList(); + } + private List identityMismatchDiagnostics( final Optional registryEntry, final PackerAssetDeclaration declaration, diff --git a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerReadMessageMapper.java b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerReadMessageMapper.java index 83054b5a..c1fc78a3 100644 --- a/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerReadMessageMapper.java +++ b/prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerReadMessageMapper.java @@ -30,6 +30,7 @@ public final class PackerReadMessageMapper { details.outputCodec(), details.availableOutputCodecs(), toCodecConfigurationFieldsByCodecDTO(details.codecConfigurationFieldsByCodec()), + toCodecConfigurationFieldDTOs(details.metadataFields()), details.inputsByRole(), toDiagnosticDTOs(details.diagnostics())); } @@ -74,6 +75,11 @@ public final class PackerReadMessageMapper { field.options()); } + private static List toCodecConfigurationFieldDTOs( + List fields) { + return fields.stream().map(PackerReadMessageMapper::toCodecConfigurationFieldDTO).toList(); + } + private static PackerDiagnosticDTO toDiagnosticDTO(PackerDiagnostic diagnostic) { return new PackerDiagnosticDTO( diagnostic.severity(), diff --git a/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/FileSystemPackerWorkspaceServiceTest.java b/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/FileSystemPackerWorkspaceServiceTest.java index e739ba53..3d8f8a37 100644 --- a/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/FileSystemPackerWorkspaceServiceTest.java +++ b/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/FileSystemPackerWorkspaceServiceTest.java @@ -191,7 +191,10 @@ final class FileSystemPackerWorkspaceServiceTest { OutputCodecCatalog.NONE, Map.of( "NONE:packMode", "tight", - "NONE:palette", "mono"))); + "NONE:palette", "mono"), + Map.of( + "tile_size", "16", + "channels", "1"))); assertTrue(result.success()); assertNull(result.errorMessage()); @@ -202,6 +205,8 @@ final class FileSystemPackerWorkspaceServiceTest { assertEquals("NONE", manifest.path("output").path("codec").asText()); assertEquals("tight", manifest.path("output").path("codec_configuration").path("packMode").asText()); assertEquals("mono", manifest.path("output").path("codec_configuration").path("palette").asText()); + assertEquals("16", manifest.path("output").path("metadata").path("tile_size").asText()); + assertEquals("1", manifest.path("output").path("metadata").path("channels").asText()); } @Test @@ -218,7 +223,8 @@ final class FileSystemPackerWorkspaceServiceTest { AssetReference.forAssetId(1), false, OutputCodecCatalog.NONE, - Map.of("NONE:packMode", "tight"))); + Map.of("NONE:packMode", "tight"), + Map.of())); assertTrue(updateResult.success()); assertEquals(1, loader.loadCount()); @@ -243,6 +249,7 @@ final class FileSystemPackerWorkspaceServiceTest { AssetReference.forAssetId(1), true, OutputCodecCatalog.NONE, + Map.of(), Map.of())); assertFalse(result.success()); diff --git a/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/PackerAssetDetailsServiceTest.java b/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/PackerAssetDetailsServiceTest.java index 26c7ca55..0d2d8d24 100644 --- a/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/PackerAssetDetailsServiceTest.java +++ b/prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/PackerAssetDetailsServiceTest.java @@ -1,6 +1,7 @@ package p.packer.services; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import p.packer.messages.AssetReference; @@ -16,6 +17,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Comparator; import java.util.List; +import java.util.Map; import static org.junit.jupiter.api.Assertions.*; @@ -57,6 +59,30 @@ final class PackerAssetDetailsServiceTest { assertEquals(List.of(), result.details().codecConfigurationFieldsByCodec().get(OutputCodecCatalog.NONE)); } + @Test + void exposesMetadataFieldsFromOutputMetadataContract() throws Exception { + final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("managed-with-metadata")); + final Path manifestPath = projectRoot.resolve("assets/ui/atlas/asset.json"); + + final ObjectMapper mapper = new ObjectMapper(); + final var manifest = mapper.readTree(manifestPath.toFile()); + final ObjectNode output = (ObjectNode) manifest.path("output"); + final ObjectNode metadata = output.putObject("metadata"); + metadata.put("tile_size", "16"); + metadata.put("channels", "1"); + mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest); + + final PackerAssetDetailsService service = service(); + final var result = service.getAssetDetails(new GetAssetDetailsRequest(project(projectRoot), AssetReference.forAssetId(1))); + + assertEquals(PackerOperationStatus.SUCCESS, result.status()); + assertEquals( + Map.of("tile_size", "16", "channels", "1"), + result.details().metadataFields().stream().collect(java.util.stream.Collectors.toMap( + field -> field.key(), + field -> field.value()))); + } + @Test void returnsInvalidDetailsForInvalidDeclaration() throws Exception { final Path projectRoot = copyFixture("workspaces/invalid-missing-fields", tempDir.resolve("invalid")); diff --git a/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java b/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java index d460702e..4f8bdb9e 100644 --- a/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java +++ b/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java @@ -97,6 +97,7 @@ public enum I18n { ASSETS_SECTION_SUMMARY("assets.section.summary"), ASSETS_SECTION_RUNTIME_CONTRACT("assets.section.runtimeContract"), ASSETS_SUBSECTION_CODEC_CONFIGURATION("assets.subsection.codecConfiguration"), + ASSETS_SUBSECTION_METADATA("assets.subsection.metadata"), ASSETS_SECTION_INPUTS_PREVIEW("assets.section.inputsPreview"), ASSETS_SECTION_DIAGNOSTICS("assets.section.diagnostics"), ASSETS_SECTION_ACTIONS("assets.section.actions"), @@ -177,6 +178,7 @@ public enum I18n { ASSETS_DETAILS_READY("assets.details.ready"), ASSETS_DETAILS_NO_SELECTION("assets.details.noSelection"), ASSETS_DETAILS_CODEC_CONFIGURATION_EMPTY("assets.details.codecConfiguration.empty"), + ASSETS_DETAILS_METADATA_EMPTY("assets.details.metadata.empty"), 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"), diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsControl.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsControl.java index f714d18f..cf172d54 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsControl.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsControl.java @@ -416,6 +416,7 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware details.outputCodec(), details.availableOutputCodecs(), details.codecConfigurationFieldsByCodec(), + details.metadataFields(), Map.copyOf(details.inputsByRole())); } } diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/contract/AssetContractDraft.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/contract/AssetContractDraft.java index fb75a2e6..160243c1 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/contract/AssetContractDraft.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/contract/AssetContractDraft.java @@ -14,45 +14,60 @@ public record AssetContractDraft( boolean preload, OutputFormatCatalog bank, OutputCodecCatalog selectedCodec, - Map codecFieldValues) { + Map codecFieldValues, + Map metadataFieldValues) { public AssetContractDraft { Objects.requireNonNull(assetReference, "assetReference"); bank = Objects.requireNonNullElse(bank, OutputFormatCatalog.UNKNOWN); selectedCodec = Objects.requireNonNullElse(selectedCodec, OutputCodecCatalog.UNKNOWN); codecFieldValues = Map.copyOf(Objects.requireNonNull(codecFieldValues, "codecFieldValues")); + metadataFieldValues = Map.copyOf(Objects.requireNonNull(metadataFieldValues, "metadataFieldValues")); } public static AssetContractDraft fromDetails(AssetWorkspaceAssetDetails details) { final Map codecFieldValues = new LinkedHashMap<>(); details.codecConfigurationFieldsByCodec().forEach((codec, fields) -> fields.forEach(field -> codecFieldValues.put(keyOf(codec, field.key()), field.value()))); + final Map metadataFieldValues = new LinkedHashMap<>(); + details.metadataFields().forEach(field -> metadataFieldValues.put(field.key(), field.value())); return new AssetContractDraft( details.summary().assetReference(), details.summary().preload(), details.outputFormat(), details.outputCodec(), - codecFieldValues); + codecFieldValues, + metadataFieldValues); } public AssetContractDraft withPreload(boolean preload) { - return new AssetContractDraft(assetReference, preload, bank, selectedCodec, codecFieldValues); + return new AssetContractDraft(assetReference, preload, bank, selectedCodec, codecFieldValues, metadataFieldValues); } public AssetContractDraft withSelectedCodec(OutputCodecCatalog selectedCodec) { - return new AssetContractDraft(assetReference, preload, bank, selectedCodec, codecFieldValues); + return new AssetContractDraft(assetReference, preload, bank, selectedCodec, codecFieldValues, metadataFieldValues); } public AssetContractDraft withCodecFieldValue(OutputCodecCatalog codec, String fieldKey, String value) { final Map nextValues = new LinkedHashMap<>(codecFieldValues); nextValues.put(keyOf(codec, fieldKey), Objects.requireNonNullElse(value, "")); - return new AssetContractDraft(assetReference, preload, bank, selectedCodec, nextValues); + return new AssetContractDraft(assetReference, preload, bank, selectedCodec, nextValues, metadataFieldValues); + } + + public AssetContractDraft withMetadataFieldValue(String fieldKey, String value) { + final Map nextValues = new LinkedHashMap<>(metadataFieldValues); + nextValues.put(fieldKey, Objects.requireNonNullElse(value, "")); + return new AssetContractDraft(assetReference, preload, bank, selectedCodec, codecFieldValues, nextValues); } public String codecFieldValue(OutputCodecCatalog codec, String fieldKey, String fallback) { return codecFieldValues.getOrDefault(keyOf(codec, fieldKey), Objects.requireNonNullElse(fallback, "")); } + public String metadataFieldValue(String fieldKey, String fallback) { + return metadataFieldValues.getOrDefault(fieldKey, Objects.requireNonNullElse(fallback, "")); + } + private static String keyOf(OutputCodecCatalog codec, String fieldKey) { return codec.name() + ":" + fieldKey; } diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/contract/AssetDetailsContractControl.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/contract/AssetDetailsContractControl.java index ff30b394..aba5e52b 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/contract/AssetDetailsContractControl.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/details/contract/AssetDetailsContractControl.java @@ -105,7 +105,8 @@ public final class AssetDetailsContractControl extends VBox implements StudioCon createPreloadEditor(draft, editing)), AssetDetailsUiSupport.createKeyValueRow( Container.i18n().text(I18n.ASSETS_LABEL_BANK), - draft.bank())); + draft.bank()), + createMetadataSection(details, draft, editing)); final VBox codecColumn = new VBox(10); codecColumn.getStyleClass().addAll("assets-details-contract-column", "assets-details-contract-codec-column"); @@ -220,6 +221,110 @@ public final class AssetDetailsContractControl extends VBox implements StudioCon return createCodecMetadataPane(title, content); } + private Node createMetadataSection( + AssetWorkspaceAssetDetails details, + AssetContractDraft draft, + boolean editing) { + final VBox subsection = new VBox(8); + subsection.getStyleClass().add("assets-details-contract-metadata"); + final Label title = new Label(Container.i18n().text(I18n.ASSETS_SUBSECTION_METADATA)); + title.getStyleClass().add("assets-details-subsection-title"); + final VBox content = new VBox(8); + content.getStyleClass().add("assets-details-contract-metadata-content"); + + if (details.metadataFields().isEmpty()) { + content.getChildren().add(AssetDetailsUiSupport.createSectionMessage( + Container.i18n().text(I18n.ASSETS_DETAILS_METADATA_EMPTY))); + return createCodecMetadataPane(title, content); + } + + for (PackerCodecConfigurationFieldDTO field : details.metadataFields()) { + content.getChildren().add(createMetadataFieldRow(field, draft, editing)); + } + return createCodecMetadataPane(title, content); + } + + private Node createMetadataFieldRow( + PackerCodecConfigurationFieldDTO field, + AssetContractDraft draft, + boolean editing) { + final Node valueNode = switch (field.fieldType()) { + case BOOLEAN -> createMetadataBooleanField(field, draft, editing); + case ENUM -> createMetadataEnumField(field, draft, editing); + case INTEGER, TEXT -> createMetadataTextField(field, draft, editing); + }; + return AssetDetailsUiSupport.createKeyValueRow(field.label(), valueNode); + } + + private Node createMetadataBooleanField( + PackerCodecConfigurationFieldDTO field, + AssetContractDraft draft, + boolean editing) { + final CheckBox checkBox = new CheckBox(); + checkBox.getStyleClass().add("assets-details-readonly-check"); + checkBox.setSelected(Boolean.parseBoolean(currentMetadataValue(field, draft))); + if (editing) { + checkBox.selectedProperty().addListener((ignored, oldValue, newValue) -> { + if (!Objects.equals(oldValue, newValue)) { + formSession.updateDraft(current -> current.withMetadataFieldValue( + field.key(), + Boolean.toString(newValue))); + actionBar.updateState(formSession.mode(), formSession.isDirty()); + } + }); + } else { + configureReadOnlyControl(checkBox); + } + return checkBox; + } + + private Node createMetadataEnumField( + PackerCodecConfigurationFieldDTO field, + AssetContractDraft draft, + boolean editing) { + final ComboBox comboBox = new ComboBox<>(FXCollections.observableArrayList(field.options())); + comboBox.getStyleClass().add("assets-details-combo"); + comboBox.setMaxWidth(Double.MAX_VALUE); + comboBox.getSelectionModel().select(currentMetadataValue(field, draft)); + if (editing) { + comboBox.valueProperty().addListener((ignored, oldValue, newValue) -> { + if (newValue != null && !Objects.equals(oldValue, newValue)) { + formSession.updateDraft(current -> current.withMetadataFieldValue( + field.key(), + newValue)); + actionBar.updateState(formSession.mode(), formSession.isDirty()); + } + }); + } else { + configureReadOnlyControl(comboBox); + } + return comboBox; + } + + private Node createMetadataTextField( + PackerCodecConfigurationFieldDTO field, + AssetContractDraft draft, + boolean editing) { + final TextField textField = new TextField(currentMetadataValue(field, draft)); + textField.getStyleClass().add("assets-workspace-search"); + textField.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(textField, Priority.ALWAYS); + if (editing) { + textField.textProperty().addListener((ignored, oldValue, newValue) -> { + if (!Objects.equals(oldValue, newValue)) { + formSession.updateDraft(current -> current.withMetadataFieldValue( + field.key(), + Objects.requireNonNullElse(newValue, ""))); + actionBar.updateState(formSession.mode(), formSession.isDirty()); + } + }); + } else { + textField.setEditable(false); + configureReadOnlyControl(textField); + } + return textField; + } + private Node createCodecMetadataPane(Label title, VBox content) { final ScrollPane scrollPane = new ScrollPane(content); scrollPane.setFitToWidth(true); @@ -320,6 +425,10 @@ public final class AssetDetailsContractControl extends VBox implements StudioCon return draft.codecFieldValue(draft.selectedCodec(), field.key(), field.value()); } + private String currentMetadataValue(PackerCodecConfigurationFieldDTO field, AssetContractDraft draft) { + return draft.metadataFieldValue(field.key(), field.value()); + } + private void configureReadOnlyControl(javafx.scene.control.Control control) { control.setMouseTransparent(true); control.setFocusTraversable(false); @@ -344,7 +453,8 @@ public final class AssetDetailsContractControl extends VBox implements StudioCon draft.assetReference(), draft.preload(), draft.selectedCodec(), - draft.codecFieldValues()); + draft.codecFieldValues(), + draft.metadataFieldValues()); try { final var response = Container.packer().workspaceService().updateAssetContract(request); if (response.success()) { diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/AssetWorkspaceAssetDetails.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/AssetWorkspaceAssetDetails.java index 777c320e..d577981f 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/AssetWorkspaceAssetDetails.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/AssetWorkspaceAssetDetails.java @@ -16,6 +16,7 @@ public record AssetWorkspaceAssetDetails( OutputCodecCatalog outputCodec, List availableOutputCodecs, Map> codecConfigurationFieldsByCodec, + List metadataFields, Map> inputsByRole) { public AssetWorkspaceAssetDetails { @@ -25,6 +26,7 @@ public record AssetWorkspaceAssetDetails( outputCodec = Objects.requireNonNullElse(outputCodec, OutputCodecCatalog.UNKNOWN); availableOutputCodecs = List.copyOf(Objects.requireNonNull(availableOutputCodecs, "availableOutputCodecs")); codecConfigurationFieldsByCodec = Map.copyOf(Objects.requireNonNull(codecConfigurationFieldsByCodec, "codecConfigurationFieldsByCodec")); + metadataFields = List.copyOf(Objects.requireNonNull(metadataFields, "metadataFields")); inputsByRole = Map.copyOf(Objects.requireNonNull(inputsByRole, "inputsByRole")); } } diff --git a/prometeu-studio/src/main/resources/i18n/messages.properties b/prometeu-studio/src/main/resources/i18n/messages.properties index 9368c8b7..931ca2c3 100644 --- a/prometeu-studio/src/main/resources/i18n/messages.properties +++ b/prometeu-studio/src/main/resources/i18n/messages.properties @@ -87,6 +87,7 @@ assets.badge.diagnostics=Diagnostics assets.section.summary=Summary assets.section.runtimeContract=Runtime Contract assets.subsection.codecConfiguration=Codec Configuration +assets.subsection.metadata=Metadata assets.section.inputsPreview=Inputs / Preview assets.section.diagnostics=Diagnostics assets.section.actions=Actions @@ -168,6 +169,7 @@ assets.details.empty=Create or add assets to this project to start using the Ass 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.codecConfiguration.empty=This codec does not expose configuration fields yet. +assets.details.metadata.empty=This asset does not expose metadata fields yet. 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 diff --git a/test-projects/main/.studio/activities.json b/test-projects/main/.studio/activities.json index 8b5adaa5..b61e764e 100644 --- a/test-projects/main/.studio/activities.json +++ b/test-projects/main/.studio/activities.json @@ -748,6 +748,56 @@ "message" : "Asset scan started", "severity" : "INFO", "sticky" : false +}, { + "source" : "Assets", + "message" : "8 assets loaded", + "severity" : "SUCCESS", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: test", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: bla", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: one-more-atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: ui_atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: one-more-atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: bbb2", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: ui_atlas", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Discovered asset: Bigode", + "severity" : "INFO", + "sticky" : false +}, { + "source" : "Assets", + "message" : "Asset scan started", + "severity" : "INFO", + "sticky" : false }, { "source" : "Assets", "message" : "Asset moved: bbb2 -> recovered/bbb2", @@ -2448,54 +2498,4 @@ "message" : "Discovered asset: bla", "severity" : "INFO", "sticky" : false -}, { - "source" : "Assets", - "message" : "Discovered asset: one-more-atlas", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Discovered asset: ui_atlas", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Discovered asset: Bigode", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Asset scan started", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "7 assets loaded", - "severity" : "SUCCESS", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Discovered asset: test", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Discovered asset: one-more-atlas", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Discovered asset: ui_atlas", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Discovered asset: bla", - "severity" : "INFO", - "sticky" : false -}, { - "source" : "Assets", - "message" : "Discovered asset: one-more-atlas", - "severity" : "INFO", - "sticky" : false } ] \ No newline at end of file