From 1cff72adcea727abc50a3ad0346cf755f283b2a7 Mon Sep 17 00:00:00 2001 From: Jon Tyson <6943745+jahjedtieson@users.noreply.github.com> Date: Tue, 8 Mar 2022 01:32:15 -0800 Subject: [PATCH 01/31] DBAPI / JOB: * Update model test cases with latest output from Cook's si-packrat-inspect --- server/tests/db/composite/Model.setup.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/server/tests/db/composite/Model.setup.ts b/server/tests/db/composite/Model.setup.ts index d64cf0e59..704f44648 100644 --- a/server/tests/db/composite/Model.setup.ts +++ b/server/tests/db/composite/Model.setup.ts @@ -102,17 +102,17 @@ const modelTestFiles: ModelTestFile[] = [ // Note, when extracted from logging the expected JSON below needs to have escaping added // to the color elements below ... replace color: \"0, 0, 0\" with color: \\"0, 0, 0\\" const modelTestCaseInspectJSONMap: Map = new Map([ - ['fbx-stand-alone', '{"success":true,"error":"","modelConstellation":{"Model":{"Name":"eremotherium_laurillardi-150k-4096.fbx","DateCreated":"2021-04-01T00:00:00.000Z","idVCreationMethod":0,"idVModality":0,"idVUnits":0,"idVPurpose":0,"idVFileType":59,"idAssetThumbnail":null,"CountAnimations":0,"CountCameras":0,"CountFaces":149999,"CountLights":0,"CountMaterials":1,"CountMeshes":1,"CountVertices":104561,"CountEmbeddedTextures":1,"CountLinkedTextures":1,"FileEncoding":"BINARY","IsDracoCompressed":false,"AutomationTag":null,"CountTriangles":149999,"idModel":1},"ModelObjects":[{"idModelObject":1,"idModel":1,"BoundingBoxP1X":-892.2620849609375,"BoundingBoxP1Y":-2167.86767578125,"BoundingBoxP1Z":-971.3925170898438,"BoundingBoxP2X":892.2653198242188,"BoundingBoxP2Y":2167.867919921875,"BoundingBoxP2Z":971.3912963867188,"CountVertices":104561,"CountFaces":149999,"CountColorChannels":0,"CountTextureCoordinateChannels":1,"HasBones":false,"HasFaceNormals":true,"HasTangents":null,"HasTextureCoordinates":true,"HasVertexNormals":null,"HasVertexColor":false,"IsTwoManifoldUnbounded":false,"IsTwoManifoldBounded":true,"IsWatertight":false,"SelfIntersecting":true,"CountTriangles":149999}],"ModelMaterials":[{"idModelMaterial":1,"Name":"material_0"}],"ModelMaterialChannels":[{"idModelMaterialChannel":1,"idModelMaterial":1,"idVMaterialType":64,"MaterialTypeOther":null,"idModelMaterialUVMap":1,"UVMapEmbedded":false,"ChannelPosition":0,"ChannelWidth":3,"Scalar1":1,"Scalar2":1,"Scalar3":1,"Scalar4":null,"AdditionalAttributes":null,"Source":"../../Users/blundellj/Downloads/eremotherium_laurillardi-150k-4096-obj/eremotherium_laurillardi-150k-4096-diffuse.jpg"}],"ModelMaterialUVMaps":[{"idModel":1,"idAsset":2,"UVMapEdgeLength":0,"idModelMaterialUVMap":1}],"ModelObjectModelMaterialXref":[{"idModelObjectModelMaterialXref":1,"idModelObject":1,"idModelMaterial":1}],"ModelAssets":[{"Asset":{"FileName":"eremotherium_laurillardi-150k-4096.fbx","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":1},"AssetVersion":{"idAsset":1,"Version":0,"FileName":"eremotherium_laurillardi-150k-4096.fbx","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":1,"IngestedOrig":null},"AssetName":"eremotherium_laurillardi-150k-4096.fbx","AssetType":"Model"},{"Asset":{"FileName":"../../Users/blundellj/Downloads/eremotherium_laurillardi-150k-4096-obj/eremotherium_laurillardi-150k-4096-diffuse.jpg","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":2},"AssetVersion":{"idAsset":2,"Version":0,"FileName":"../../Users/blundellj/Downloads/eremotherium_laurillardi-150k-4096-obj/eremotherium_laurillardi-150k-4096-diffuse.jpg","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":2,"IngestedOrig":null},"AssetName":"../../Users/blundellj/Downloads/eremotherium_laurillardi-150k-4096-obj/eremotherium_laurillardi-150k-4096-diffuse.jpg","AssetType":"Texture Map diffuse"}]}}'], - ['fbx-with-support', '{"success":true,"error":"","modelConstellation":{"Model":{"Name":"eremotherium_laurillardi-150k-4096.fbx","DateCreated":"2021-04-01T00:00:00.000Z","idVCreationMethod":0,"idVModality":0,"idVUnits":0,"idVPurpose":0,"idVFileType":59,"idAssetThumbnail":null,"CountAnimations":0,"CountCameras":0,"CountFaces":149999,"CountLights":0,"CountMaterials":1,"CountMeshes":1,"CountVertices":74796,"CountEmbeddedTextures":0,"CountLinkedTextures":1,"FileEncoding":"BINARY","IsDracoCompressed":false,"AutomationTag":null,"CountTriangles":149999,"idModel":1},"ModelObjects":[{"idModelObject":1,"idModel":1,"BoundingBoxP1X":-892.2620849609375,"BoundingBoxP1Y":-2167.86767578125,"BoundingBoxP1Z":-971.3925170898438,"BoundingBoxP2X":892.2653198242188,"BoundingBoxP2Y":2167.867919921875,"BoundingBoxP2Z":971.3912963867188,"CountVertices":74796,"CountFaces":149999,"CountColorChannels":0,"CountTextureCoordinateChannels":1,"HasBones":false,"HasFaceNormals":true,"HasTangents":null,"HasTextureCoordinates":true,"HasVertexNormals":null,"HasVertexColor":false,"IsTwoManifoldUnbounded":false,"IsTwoManifoldBounded":false,"IsWatertight":false,"SelfIntersecting":true,"CountTriangles":149999}],"ModelMaterials":[{"idModelMaterial":1,"Name":"material_0"}],"ModelMaterialChannels":[{"idModelMaterialChannel":1,"idModelMaterial":1,"idVMaterialType":64,"MaterialTypeOther":null,"idModelMaterialUVMap":1,"UVMapEmbedded":false,"ChannelPosition":0,"ChannelWidth":3,"Scalar1":0.8,"Scalar2":0.8,"Scalar3":0.8,"Scalar4":null,"AdditionalAttributes":null,"Source":"/Users/blundellj/OneDrive - Smithsonian Institution/Packrat demo files/model validation demo files/eremotherium_laurillardi-150k-4096-obj/eremotherium_laurillardi-150k-4096-diffuse.jpg"}],"ModelMaterialUVMaps":[{"idModel":1,"idAsset":2,"UVMapEdgeLength":0,"idModelMaterialUVMap":1}],"ModelObjectModelMaterialXref":[{"idModelObjectModelMaterialXref":1,"idModelObject":1,"idModelMaterial":1}],"ModelAssets":[{"Asset":{"FileName":"eremotherium_laurillardi-150k-4096.fbx","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":1},"AssetVersion":{"idAsset":1,"Version":0,"FileName":"eremotherium_laurillardi-150k-4096.fbx","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":1,"IngestedOrig":null},"AssetName":"eremotherium_laurillardi-150k-4096.fbx","AssetType":"Model"},{"Asset":{"FileName":"/Users/blundellj/OneDrive - Smithsonian Institution/Packrat demo files/model validation demo files/eremotherium_laurillardi-150k-4096-obj/eremotherium_laurillardi-150k-4096-diffuse.jpg","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":2},"AssetVersion":{"idAsset":2,"Version":0,"FileName":"/Users/blundellj/OneDrive - Smithsonian Institution/Packrat demo files/model validation demo files/eremotherium_laurillardi-150k-4096-obj/eremotherium_laurillardi-150k-4096-diffuse.jpg","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":2,"IngestedOrig":null},"AssetName":"/Users/blundellj/OneDrive - Smithsonian Institution/Packrat demo files/model validation demo files/eremotherium_laurillardi-150k-4096-obj/eremotherium_laurillardi-150k-4096-diffuse.jpg","AssetType":"Texture Map diffuse"}]}}'], - ['glb', '{"success":true,"error":"","modelConstellation":{"Model":{"Name":"eremotherium_laurillardi-150k-4096.glb","DateCreated":"2021-04-01T00:00:00.000Z","idVCreationMethod":0,"idVModality":0,"idVUnits":0,"idVPurpose":0,"idVFileType":52,"idAssetThumbnail":null,"CountAnimations":0,"CountCameras":0,"CountFaces":149999,"CountLights":0,"CountMaterials":1,"CountMeshes":1,"CountVertices":104561,"CountEmbeddedTextures":1,"CountLinkedTextures":0,"FileEncoding":"BINARY","IsDracoCompressed":false,"AutomationTag":null,"CountTriangles":149999,"idModel":1},"ModelObjects":[{"idModelObject":1,"idModel":1,"BoundingBoxP1X":-892.2620849609375,"BoundingBoxP1Y":-2167.867431640625,"BoundingBoxP1Z":-971.3921508789062,"BoundingBoxP2X":892.2653198242188,"BoundingBoxP2Y":2167.86767578125,"BoundingBoxP2Z":971.3909301757812,"CountVertices":104561,"CountFaces":149999,"CountColorChannels":0,"CountTextureCoordinateChannels":1,"HasBones":false,"HasFaceNormals":true,"HasTangents":null,"HasTextureCoordinates":true,"HasVertexNormals":null,"HasVertexColor":false,"IsTwoManifoldUnbounded":false,"IsTwoManifoldBounded":true,"IsWatertight":false,"SelfIntersecting":true,"CountTriangles":149999}],"ModelMaterials":[{"idModelMaterial":1,"Name":"material_0"}],"ModelMaterialChannels":[{"idModelMaterialChannel":1,"idModelMaterial":1,"idVMaterialType":64,"MaterialTypeOther":null,"idModelMaterialUVMap":null,"UVMapEmbedded":true,"ChannelPosition":null,"ChannelWidth":null,"Scalar1":1,"Scalar2":1,"Scalar3":1,"Scalar4":1,"AdditionalAttributes":null}],"ModelMaterialUVMaps":null,"ModelObjectModelMaterialXref":[{"idModelObjectModelMaterialXref":1,"idModelObject":1,"idModelMaterial":1}],"ModelAssets":[{"Asset":{"FileName":"eremotherium_laurillardi-150k-4096.glb","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":1},"AssetVersion":{"idAsset":1,"Version":0,"FileName":"eremotherium_laurillardi-150k-4096.glb","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":1,"IngestedOrig":null},"AssetName":"eremotherium_laurillardi-150k-4096.glb","AssetType":"Model"}]}}'], - ['glb-draco', '{"success":true,"error":"","modelConstellation":{"Model":{"Name":"eremotherium_laurillardi-Part-100k-512.glb","DateCreated":"2021-04-01T00:00:00.000Z","idVCreationMethod":0,"idVModality":0,"idVUnits":0,"idVPurpose":0,"idVFileType":52,"idAssetThumbnail":null,"CountAnimations":0,"CountCameras":0,"CountFaces":99766,"CountLights":0,"CountMaterials":1,"CountMeshes":1,"CountVertices":64464,"CountEmbeddedTextures":3,"CountLinkedTextures":0,"FileEncoding":"BINARY","IsDracoCompressed":true,"AutomationTag":null,"CountTriangles":99766,"idModel":1},"ModelObjects":[{"idModelObject":1,"idModel":1,"BoundingBoxP1X":-68.70997619628906,"BoundingBoxP1Y":-167.00453186035156,"BoundingBoxP1Z":-61.58316421508789,"BoundingBoxP2X":56.08637237548828,"BoundingBoxP2Y":14.136469841003418,"BoundingBoxP2Z":117.65611267089844,"CountVertices":64464,"CountFaces":99766,"CountColorChannels":0,"CountTextureCoordinateChannels":1,"HasBones":false,"HasFaceNormals":true,"HasTangents":null,"HasTextureCoordinates":true,"HasVertexNormals":null,"HasVertexColor":false,"IsTwoManifoldUnbounded":false,"IsTwoManifoldBounded":true,"IsWatertight":false,"SelfIntersecting":true,"CountTriangles":99766}],"ModelMaterials":[{"idModelMaterial":1,"Name":"default"}],"ModelMaterialChannels":[{"idModelMaterialChannel":1,"idModelMaterial":1,"idVMaterialType":64,"MaterialTypeOther":null,"idModelMaterialUVMap":null,"UVMapEmbedded":true,"ChannelPosition":null,"ChannelWidth":null,"Scalar1":1,"Scalar2":1,"Scalar3":1,"Scalar4":1,"AdditionalAttributes":null},{"idModelMaterialChannel":2,"idModelMaterial":1,"idVMaterialType":69,"MaterialTypeOther":null,"idModelMaterialUVMap":null,"UVMapEmbedded":true,"ChannelPosition":null,"ChannelWidth":null,"Scalar1":null,"Scalar2":null,"Scalar3":null,"Scalar4":null,"AdditionalAttributes":null},{"idModelMaterialChannel":3,"idModelMaterial":1,"idVMaterialType":73,"MaterialTypeOther":null,"idModelMaterialUVMap":null,"UVMapEmbedded":true,"ChannelPosition":null,"ChannelWidth":null,"Scalar1":null,"Scalar2":null,"Scalar3":null,"Scalar4":null,"AdditionalAttributes":null}],"ModelMaterialUVMaps":null,"ModelObjectModelMaterialXref":[{"idModelObjectModelMaterialXref":1,"idModelObject":1,"idModelMaterial":1}],"ModelAssets":[{"Asset":{"FileName":"eremotherium_laurillardi-Part-100k-512.glb","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":1},"AssetVersion":{"idAsset":1,"Version":0,"FileName":"eremotherium_laurillardi-Part-100k-512.glb","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":1,"IngestedOrig":null},"AssetName":"eremotherium_laurillardi-Part-100k-512.glb","AssetType":"Model"}]}}'], - ['obj', '{"success":true,"error":"","modelConstellation":{"Model":{"Name":"eremotherium_laurillardi-150k-4096.obj","DateCreated":"2021-04-01T00:00:00.000Z","idVCreationMethod":0,"idVModality":0,"idVUnits":0,"idVPurpose":0,"idVFileType":49,"idAssetThumbnail":null,"CountAnimations":0,"CountCameras":0,"CountFaces":149999,"CountLights":0,"CountMaterials":1,"CountMeshes":1,"CountVertices":74796,"CountEmbeddedTextures":0,"CountLinkedTextures":1,"FileEncoding":"ASCII","IsDracoCompressed":false,"AutomationTag":null,"CountTriangles":149999,"idModel":1},"ModelObjects":[{"idModelObject":1,"idModel":1,"BoundingBoxP1X":-892.2620849609375,"BoundingBoxP1Y":-971.3923950195312,"BoundingBoxP1Z":-2167.867919921875,"BoundingBoxP2X":892.2653198242188,"BoundingBoxP2Y":971.3911743164062,"BoundingBoxP2Z":2167.86767578125,"CountVertices":74796,"CountFaces":149999,"CountColorChannels":0,"CountTextureCoordinateChannels":1,"HasBones":false,"HasFaceNormals":true,"HasTangents":null,"HasTextureCoordinates":true,"HasVertexNormals":null,"HasVertexColor":false,"IsTwoManifoldUnbounded":false,"IsTwoManifoldBounded":false,"IsWatertight":false,"SelfIntersecting":true,"CountTriangles":149999}],"ModelMaterials":[{"idModelMaterial":1,"Name":"material_0"}],"ModelMaterialChannels":[{"idModelMaterialChannel":1,"idModelMaterial":1,"idVMaterialType":64,"MaterialTypeOther":null,"idModelMaterialUVMap":1,"UVMapEmbedded":false,"ChannelPosition":0,"ChannelWidth":3,"Scalar1":0.6,"Scalar2":0.6,"Scalar3":0.6,"Scalar4":null,"AdditionalAttributes":null,"Source":"eremotherium_laurillardi-150k-4096-diffuse.jpg"}],"ModelMaterialUVMaps":[{"idModel":1,"idAsset":2,"UVMapEdgeLength":0,"idModelMaterialUVMap":1}],"ModelObjectModelMaterialXref":[{"idModelObjectModelMaterialXref":1,"idModelObject":1,"idModelMaterial":1}],"ModelAssets":[{"Asset":{"FileName":"eremotherium_laurillardi-150k-4096.obj","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":1},"AssetVersion":{"idAsset":1,"Version":0,"FileName":"eremotherium_laurillardi-150k-4096.obj","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":1,"IngestedOrig":null},"AssetName":"eremotherium_laurillardi-150k-4096.obj","AssetType":"Model"},{"Asset":{"FileName":"eremotherium_laurillardi-150k-4096-diffuse.jpg","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":2},"AssetVersion":{"idAsset":2,"Version":0,"FileName":"eremotherium_laurillardi-150k-4096-diffuse.jpg","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":2,"IngestedOrig":null},"AssetName":"eremotherium_laurillardi-150k-4096-diffuse.jpg","AssetType":"Texture Map diffuse"}]}}'], - ['ply', '{"success":true,"error":"","modelConstellation":{"Model":{"Name":"eremotherium_laurillardi-150k.ply","DateCreated":"2021-04-01T00:00:00.000Z","idVCreationMethod":0,"idVModality":0,"idVUnits":0,"idVPurpose":0,"idVFileType":50,"idAssetThumbnail":null,"CountAnimations":0,"CountCameras":0,"CountFaces":149999,"CountLights":0,"CountMaterials":1,"CountMeshes":1,"CountVertices":74796,"CountEmbeddedTextures":0,"CountLinkedTextures":1,"FileEncoding":"BINARY","IsDracoCompressed":false,"AutomationTag":null,"CountTriangles":149999,"idModel":1},"ModelObjects":[{"idModelObject":1,"idModel":1,"BoundingBoxP1X":-892.2620849609375,"BoundingBoxP1Y":-971.3921508789062,"BoundingBoxP1Z":-2167.86767578125,"BoundingBoxP2X":892.2653198242188,"BoundingBoxP2Y":971.3909301757812,"BoundingBoxP2Z":2167.867431640625,"CountVertices":74796,"CountFaces":149999,"CountColorChannels":1,"CountTextureCoordinateChannels":0,"HasBones":false,"HasFaceNormals":false,"HasTangents":null,"HasTextureCoordinates":false,"HasVertexNormals":null,"HasVertexColor":true,"IsTwoManifoldUnbounded":false,"IsTwoManifoldBounded":false,"IsWatertight":false,"SelfIntersecting":true,"CountTriangles":149999}],"ModelMaterials":[{"idModelMaterial":1,"Name":""}],"ModelMaterialChannels":[{"idModelMaterialChannel":1,"idModelMaterial":1,"idVMaterialType":64,"MaterialTypeOther":null,"idModelMaterialUVMap":1,"UVMapEmbedded":false,"ChannelPosition":0,"ChannelWidth":3,"Scalar1":1,"Scalar2":1,"Scalar3":1,"Scalar4":1,"AdditionalAttributes":null,"Source":"eremotherium_laurillardi-150k-4096-diffuse.jpg"}],"ModelMaterialUVMaps":[{"idModel":1,"idAsset":2,"UVMapEdgeLength":0,"idModelMaterialUVMap":1}],"ModelObjectModelMaterialXref":[{"idModelObjectModelMaterialXref":1,"idModelObject":1,"idModelMaterial":1}],"ModelAssets":[{"Asset":{"FileName":"eremotherium_laurillardi-150k.ply","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":1},"AssetVersion":{"idAsset":1,"Version":0,"FileName":"eremotherium_laurillardi-150k.ply","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":1,"IngestedOrig":null},"AssetName":"eremotherium_laurillardi-150k.ply","AssetType":"Model"},{"Asset":{"FileName":"eremotherium_laurillardi-150k-4096-diffuse.jpg","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":2},"AssetVersion":{"idAsset":2,"Version":0,"FileName":"eremotherium_laurillardi-150k-4096-diffuse.jpg","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":2,"IngestedOrig":null},"AssetName":"eremotherium_laurillardi-150k-4096-diffuse.jpg","AssetType":"Texture Map diffuse"}]}}'], - ['stl', '{"success":true,"error":"","modelConstellation":{"Model":{"Name":"eremotherium_laurillardi-150k.stl","DateCreated":"2021-04-01T00:00:00.000Z","idVCreationMethod":0,"idVModality":0,"idVUnits":0,"idVPurpose":0,"idVFileType":51,"idAssetThumbnail":null,"CountAnimations":0,"CountCameras":0,"CountFaces":149999,"CountLights":0,"CountMaterials":0,"CountMeshes":1,"CountVertices":74796,"CountEmbeddedTextures":0,"CountLinkedTextures":0,"FileEncoding":"BINARY","IsDracoCompressed":false,"AutomationTag":null,"CountTriangles":149999,"idModel":1},"ModelObjects":[{"idModelObject":1,"idModel":1,"BoundingBoxP1X":-892.2620849609375,"BoundingBoxP1Y":-971.3921508789062,"BoundingBoxP1Z":-2167.86767578125,"BoundingBoxP2X":892.2653198242188,"BoundingBoxP2Y":971.3909301757812,"BoundingBoxP2Z":2167.867431640625,"CountVertices":74796,"CountFaces":149999,"CountColorChannels":0,"CountTextureCoordinateChannels":0,"HasBones":false,"HasFaceNormals":false,"HasTangents":null,"HasTextureCoordinates":false,"HasVertexNormals":null,"HasVertexColor":false,"IsTwoManifoldUnbounded":false,"IsTwoManifoldBounded":false,"IsWatertight":false,"SelfIntersecting":true,"CountTriangles":149999}],"ModelMaterials":null,"ModelMaterialChannels":null,"ModelMaterialUVMaps":null,"ModelObjectModelMaterialXref":null,"ModelAssets":[{"Asset":{"FileName":"eremotherium_laurillardi-150k.stl","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":1},"AssetVersion":{"idAsset":1,"Version":0,"FileName":"eremotherium_laurillardi-150k.stl","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":1,"IngestedOrig":null},"AssetName":"eremotherium_laurillardi-150k.stl","AssetType":"Model"}]}}'], - ['x3d', '{"success":true,"error":"","modelConstellation":{"Model":{"Name":"eremotherium_laurillardi-150k-4096.x3d","DateCreated":"2021-04-01T00:00:00.000Z","idVCreationMethod":0,"idVModality":0,"idVUnits":0,"idVPurpose":0,"idVFileType":56,"idAssetThumbnail":null,"CountAnimations":0,"CountCameras":0,"CountFaces":149999,"CountLights":0,"CountMaterials":1,"CountMeshes":1,"CountVertices":74796,"CountEmbeddedTextures":0,"CountLinkedTextures":1,"FileEncoding":"ASCII","IsDracoCompressed":false,"AutomationTag":null,"CountTriangles":149999,"idModel":1},"ModelObjects":[{"idModelObject":1,"idModel":1,"BoundingBoxP1X":-892.2651977539062,"BoundingBoxP1Y":-2167.870361328125,"BoundingBoxP1Z":-971.3923950195312,"BoundingBoxP2X":892.26220703125,"BoundingBoxP2Y":2167.870361328125,"BoundingBoxP2Z":971.391357421875,"CountVertices":74796,"CountFaces":149999,"CountColorChannels":1,"CountTextureCoordinateChannels":1,"HasBones":false,"HasFaceNormals":false,"HasTangents":null,"HasTextureCoordinates":true,"HasVertexNormals":null,"HasVertexColor":true,"IsTwoManifoldUnbounded":false,"IsTwoManifoldBounded":false,"IsWatertight":false,"SelfIntersecting":true,"CountTriangles":149999}],"ModelMaterials":[{"idModelMaterial":1,"Name":""}],"ModelMaterialChannels":[{"idModelMaterialChannel":1,"idModelMaterial":1,"idVMaterialType":64,"MaterialTypeOther":null,"idModelMaterialUVMap":1,"UVMapEmbedded":false,"ChannelPosition":0,"ChannelWidth":3,"Scalar1":null,"Scalar2":null,"Scalar3":null,"Scalar4":null,"AdditionalAttributes":null,"Source":"eremotherium_laurillardi-150k-4096-diffuse.jpg"}],"ModelMaterialUVMaps":[{"idModel":1,"idAsset":2,"UVMapEdgeLength":0,"idModelMaterialUVMap":1}],"ModelObjectModelMaterialXref":null,"ModelAssets":[{"Asset":{"FileName":"eremotherium_laurillardi-150k-4096.x3d","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":1},"AssetVersion":{"idAsset":1,"Version":0,"FileName":"eremotherium_laurillardi-150k-4096.x3d","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":1,"IngestedOrig":null},"AssetName":"eremotherium_laurillardi-150k-4096.x3d","AssetType":"Model"},{"Asset":{"FileName":"eremotherium_laurillardi-150k-4096-diffuse.jpg","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":2},"AssetVersion":{"idAsset":2,"Version":0,"FileName":"eremotherium_laurillardi-150k-4096-diffuse.jpg","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":2,"IngestedOrig":null},"AssetName":"eremotherium_laurillardi-150k-4096-diffuse.jpg","AssetType":"Texture Map diffuse"}]}}'], - ['dae', '{"success":true,"error":"","modelConstellation":{"Model":{"Name":"clemente_helmet.dae","DateCreated":"2021-04-01T00:00:00.000Z","idVCreationMethod":0,"idVModality":0,"idVUnits":0,"idVPurpose":0,"idVFileType":58,"idAssetThumbnail":null,"CountAnimations":0,"CountCameras":0,"CountFaces":148260,"CountLights":0,"CountMaterials":1,"CountMeshes":1,"CountVertices":80417,"CountEmbeddedTextures":0,"CountLinkedTextures":1,"FileEncoding":"ASCII","IsDracoCompressed":false,"AutomationTag":null,"CountTriangles":148260,"idModel":1},"ModelObjects":[{"idModelObject":1,"idModel":1,"BoundingBoxP1X":-0.09380000084638596,"BoundingBoxP1Y":-0.13996900618076324,"BoundingBoxP1Z":-0.06898897141218185,"BoundingBoxP2X":0.09380996227264404,"BoundingBoxP2Y":0.139957994222641,"BoundingBoxP2Z":0.0689619705080986,"CountVertices":80417,"CountFaces":148260,"CountColorChannels":0,"CountTextureCoordinateChannels":1,"HasBones":false,"HasFaceNormals":false,"HasTangents":null,"HasTextureCoordinates":true,"HasVertexNormals":null,"HasVertexColor":false,"IsTwoManifoldUnbounded":false,"IsTwoManifoldBounded":true,"IsWatertight":false,"SelfIntersecting":true,"CountTriangles":148260}],"ModelMaterials":[{"idModelMaterial":1,"Name":"default"}],"ModelMaterialChannels":[{"idModelMaterialChannel":1,"idModelMaterial":1,"idVMaterialType":64,"MaterialTypeOther":null,"idModelMaterialUVMap":1,"UVMapEmbedded":false,"ChannelPosition":0,"ChannelWidth":3,"Scalar1":1,"Scalar2":1,"Scalar3":1,"Scalar4":null,"AdditionalAttributes":null,"Source":"Image_0.jpg"}],"ModelMaterialUVMaps":[{"idModel":1,"idAsset":2,"UVMapEdgeLength":0,"idModelMaterialUVMap":1}],"ModelObjectModelMaterialXref":[{"idModelObjectModelMaterialXref":1,"idModelObject":1,"idModelMaterial":1}],"ModelAssets":[{"Asset":{"FileName":"clemente_helmet.dae","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":1},"AssetVersion":{"idAsset":1,"Version":0,"FileName":"clemente_helmet.dae","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":1,"IngestedOrig":null},"AssetName":"clemente_helmet.dae","AssetType":"Model"},{"Asset":{"FileName":"Image_0.jpg","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":2},"AssetVersion":{"idAsset":2,"Version":0,"FileName":"Image_0.jpg","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":2,"IngestedOrig":null},"AssetName":"Image_0.jpg","AssetType":"Texture Map diffuse"}]}}'], - ['gltf-stand-alone', '{"success":true,"error":"","modelConstellation":{"Model":{"Name":"clemente_helmet.gltf","DateCreated":"2021-04-01T00:00:00.000Z","idVCreationMethod":0,"idVModality":0,"idVUnits":0,"idVPurpose":0,"idVFileType":53,"idAssetThumbnail":null,"CountAnimations":0,"CountCameras":0,"CountFaces":148260,"CountLights":0,"CountMaterials":1,"CountMeshes":1,"CountVertices":80417,"CountEmbeddedTextures":3,"CountLinkedTextures":0,"FileEncoding":"ASCII","IsDracoCompressed":false,"AutomationTag":null,"CountTriangles":148260,"idModel":1},"ModelObjects":[{"idModelObject":1,"idModel":1,"BoundingBoxP1X":-0.09380000084638596,"BoundingBoxP1Y":-0.13996900618076324,"BoundingBoxP1Z":-0.06898900121450424,"BoundingBoxP2X":0.09380999952554703,"BoundingBoxP2Y":0.139957994222641,"BoundingBoxP2Z":0.06896200031042099,"CountVertices":80417,"CountFaces":148260,"CountColorChannels":0,"CountTextureCoordinateChannels":1,"HasBones":false,"HasFaceNormals":true,"HasTangents":null,"HasTextureCoordinates":true,"HasVertexNormals":null,"HasVertexColor":false,"IsTwoManifoldUnbounded":false,"IsTwoManifoldBounded":true,"IsWatertight":false,"SelfIntersecting":true,"CountTriangles":148260}],"ModelMaterials":[{"idModelMaterial":1,"Name":"default"}],"ModelMaterialChannels":[{"idModelMaterialChannel":1,"idModelMaterial":1,"idVMaterialType":64,"MaterialTypeOther":null,"idModelMaterialUVMap":null,"UVMapEmbedded":true,"ChannelPosition":null,"ChannelWidth":null,"Scalar1":1,"Scalar2":1,"Scalar3":1,"Scalar4":1,"AdditionalAttributes":null},{"idModelMaterialChannel":2,"idModelMaterial":1,"idVMaterialType":69,"MaterialTypeOther":null,"idModelMaterialUVMap":null,"UVMapEmbedded":true,"ChannelPosition":null,"ChannelWidth":null,"Scalar1":null,"Scalar2":null,"Scalar3":null,"Scalar4":null,"AdditionalAttributes":null},{"idModelMaterialChannel":3,"idModelMaterial":1,"idVMaterialType":73,"MaterialTypeOther":null,"idModelMaterialUVMap":null,"UVMapEmbedded":true,"ChannelPosition":null,"ChannelWidth":null,"Scalar1":null,"Scalar2":null,"Scalar3":null,"Scalar4":null,"AdditionalAttributes":null}],"ModelMaterialUVMaps":null,"ModelObjectModelMaterialXref":[{"idModelObjectModelMaterialXref":1,"idModelObject":1,"idModelMaterial":1}],"ModelAssets":[{"Asset":{"FileName":"clemente_helmet.gltf","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":1},"AssetVersion":{"idAsset":1,"Version":0,"FileName":"clemente_helmet.gltf","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":1,"IngestedOrig":null},"AssetName":"clemente_helmet.gltf","AssetType":"Model"}]}}'], - ['gltf-with-support', '{"success":true,"error":"","modelConstellation":{"Model":{"Name":"nmah-1981_0706_06-clemente_helmet-150k-4096.gltf","DateCreated":"2021-04-01T00:00:00.000Z","idVCreationMethod":0,"idVModality":0,"idVUnits":0,"idVPurpose":0,"idVFileType":53,"idAssetThumbnail":null,"CountAnimations":0,"CountCameras":0,"CountFaces":148260,"CountLights":0,"CountMaterials":1,"CountMeshes":1,"CountVertices":80417,"CountEmbeddedTextures":0,"CountLinkedTextures":3,"FileEncoding":"ASCII","IsDracoCompressed":false,"AutomationTag":null,"CountTriangles":148260,"idModel":1},"ModelObjects":[{"idModelObject":1,"idModel":1,"BoundingBoxP1X":-0.09380000084638596,"BoundingBoxP1Y":-0.13996900618076324,"BoundingBoxP1Z":-0.06898900121450424,"BoundingBoxP2X":0.09380999952554703,"BoundingBoxP2Y":0.139957994222641,"BoundingBoxP2Z":0.06896200031042099,"CountVertices":80417,"CountFaces":148260,"CountColorChannels":0,"CountTextureCoordinateChannels":1,"HasBones":false,"HasFaceNormals":true,"HasTangents":null,"HasTextureCoordinates":true,"HasVertexNormals":null,"HasVertexColor":false,"IsTwoManifoldUnbounded":false,"IsTwoManifoldBounded":true,"IsWatertight":false,"SelfIntersecting":true,"CountTriangles":148260}],"ModelMaterials":[{"idModelMaterial":1,"Name":"default"}],"ModelMaterialChannels":[{"idModelMaterialChannel":1,"idModelMaterial":1,"idVMaterialType":64,"MaterialTypeOther":null,"idModelMaterialUVMap":1,"UVMapEmbedded":false,"ChannelPosition":0,"ChannelWidth":3,"Scalar1":1,"Scalar2":1,"Scalar3":1,"Scalar4":1,"AdditionalAttributes":null,"Source":"nmah-1981_0706_06-clemente_helmet-150k-4096-diffuse.jpg"},{"idModelMaterialChannel":2,"idModelMaterial":1,"idVMaterialType":69,"MaterialTypeOther":null,"idModelMaterialUVMap":2,"UVMapEmbedded":false,"ChannelPosition":0,"ChannelWidth":3,"Scalar1":null,"Scalar2":null,"Scalar3":null,"Scalar4":null,"AdditionalAttributes":null,"Source":"nmah-1981_0706_06-clemente_helmet-150k-4096-normals.jpg"},{"idModelMaterialChannel":3,"idModelMaterial":1,"idVMaterialType":73,"MaterialTypeOther":null,"idModelMaterialUVMap":3,"UVMapEmbedded":false,"ChannelPosition":0,"ChannelWidth":3,"Scalar1":null,"Scalar2":null,"Scalar3":null,"Scalar4":null,"AdditionalAttributes":null,"Source":"nmah-1981_0706_06-clemente_helmet-150k-4096-occlusion.jpg"}],"ModelMaterialUVMaps":[{"idModel":1,"idAsset":2,"UVMapEdgeLength":0,"idModelMaterialUVMap":1},{"idModel":1,"idAsset":3,"UVMapEdgeLength":0,"idModelMaterialUVMap":2},{"idModel":1,"idAsset":4,"UVMapEdgeLength":0,"idModelMaterialUVMap":3}],"ModelObjectModelMaterialXref":[{"idModelObjectModelMaterialXref":1,"idModelObject":1,"idModelMaterial":1}],"ModelAssets":[{"Asset":{"FileName":"nmah-1981_0706_06-clemente_helmet-150k-4096.gltf","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":1},"AssetVersion":{"idAsset":1,"Version":0,"FileName":"nmah-1981_0706_06-clemente_helmet-150k-4096.gltf","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":1,"IngestedOrig":null},"AssetName":"nmah-1981_0706_06-clemente_helmet-150k-4096.gltf","AssetType":"Model"},{"Asset":{"FileName":"nmah-1981_0706_06-clemente_helmet-150k-4096-diffuse.jpg","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":2},"AssetVersion":{"idAsset":2,"Version":0,"FileName":"nmah-1981_0706_06-clemente_helmet-150k-4096-diffuse.jpg","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":2,"IngestedOrig":null},"AssetName":"nmah-1981_0706_06-clemente_helmet-150k-4096-diffuse.jpg","AssetType":"Texture Map diffuse"},{"Asset":{"FileName":"nmah-1981_0706_06-clemente_helmet-150k-4096-normals.jpg","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":3},"AssetVersion":{"idAsset":3,"Version":0,"FileName":"nmah-1981_0706_06-clemente_helmet-150k-4096-normals.jpg","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":3,"IngestedOrig":null},"AssetName":"nmah-1981_0706_06-clemente_helmet-150k-4096-normals.jpg","AssetType":"Texture Map normal"},{"Asset":{"FileName":"nmah-1981_0706_06-clemente_helmet-150k-4096-occlusion.jpg","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":4},"AssetVersion":{"idAsset":4,"Version":0,"FileName":"nmah-1981_0706_06-clemente_helmet-150k-4096-occlusion.jpg","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":4,"IngestedOrig":null},"AssetName":"nmah-1981_0706_06-clemente_helmet-150k-4096-occlusion.jpg","AssetType":"Texture Map occlusion"}]}}'], + ['fbx-stand-alone', '{"success":true,"error":"","modelConstellation":{"Model":{"Name":"eremotherium_laurillardi-150k-4096.fbx","Title":"","DateCreated":"2021-04-01T00:00:00.000Z","idVCreationMethod":0,"idVModality":0,"idVUnits":0,"idVPurpose":0,"idVFileType":59,"idAssetThumbnail":null,"CountAnimations":0,"CountCameras":0,"CountFaces":149999,"CountLights":0,"CountMaterials":1,"CountMeshes":1,"CountVertices":104561,"CountEmbeddedTextures":1,"CountLinkedTextures":1,"FileEncoding":"BINARY","IsDracoCompressed":false,"AutomationTag":null,"CountTriangles":149999,"idModel":1},"ModelObjects":[{"idModelObject":1,"idModel":1,"BoundingBoxP1X":-892.2620849609375,"BoundingBoxP1Y":-2167.86767578125,"BoundingBoxP1Z":-971.3925170898438,"BoundingBoxP2X":892.2653198242188,"BoundingBoxP2Y":2167.867919921875,"BoundingBoxP2Z":971.3912963867188,"CountVertices":104561,"CountFaces":149999,"CountColorChannels":0,"CountTextureCoordinateChannels":1,"HasBones":false,"HasFaceNormals":true,"HasTangents":null,"HasTextureCoordinates":true,"HasVertexNormals":null,"HasVertexColor":false,"IsTwoManifoldUnbounded":false,"IsTwoManifoldBounded":true,"IsWatertight":false,"SelfIntersecting":true,"CountTriangles":149999}],"ModelMaterials":[{"idModelMaterial":1,"Name":"material_0"}],"ModelMaterialChannels":[{"idModelMaterialChannel":1,"idModelMaterial":1,"idVMaterialType":64,"MaterialTypeOther":null,"idModelMaterialUVMap":1,"UVMapEmbedded":false,"ChannelPosition":0,"ChannelWidth":3,"Scalar1":1,"Scalar2":1,"Scalar3":1,"Scalar4":null,"AdditionalAttributes":null,"Source":"../../Users/blundellj/Downloads/eremotherium_laurillardi-150k-4096-obj/eremotherium_laurillardi-150k-4096-diffuse.jpg"}],"ModelMaterialUVMaps":[{"idModel":1,"idAsset":2,"UVMapEdgeLength":0,"idModelMaterialUVMap":1}],"ModelObjectModelMaterialXref":[{"idModelObjectModelMaterialXref":1,"idModelObject":1,"idModelMaterial":1}],"ModelAssets":[{"Asset":{"FileName":"eremotherium_laurillardi-150k-4096.fbx","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":1},"AssetVersion":{"idAsset":1,"Version":0,"FileName":"eremotherium_laurillardi-150k-4096.fbx","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":1,"IngestedOrig":null},"AssetName":"eremotherium_laurillardi-150k-4096.fbx","AssetType":"Model"},{"Asset":{"FileName":"../../Users/blundellj/Downloads/eremotherium_laurillardi-150k-4096-obj/eremotherium_laurillardi-150k-4096-diffuse.jpg","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":2},"AssetVersion":{"idAsset":2,"Version":0,"FileName":"../../Users/blundellj/Downloads/eremotherium_laurillardi-150k-4096-obj/eremotherium_laurillardi-150k-4096-diffuse.jpg","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":2,"IngestedOrig":null},"AssetName":"../../Users/blundellj/Downloads/eremotherium_laurillardi-150k-4096-obj/eremotherium_laurillardi-150k-4096-diffuse.jpg","AssetType":"Texture Map diffuse"}]}}'], + ['fbx-with-support', '{"success":true,"error":"","modelConstellation":{"Model":{"Name":"eremotherium_laurillardi-150k-4096.fbx","Title":"","DateCreated":"2021-04-01T00:00:00.000Z","idVCreationMethod":0,"idVModality":0,"idVUnits":0,"idVPurpose":0,"idVFileType":59,"idAssetThumbnail":null,"CountAnimations":0,"CountCameras":0,"CountFaces":149999,"CountLights":0,"CountMaterials":1,"CountMeshes":1,"CountVertices":74796,"CountEmbeddedTextures":0,"CountLinkedTextures":1,"FileEncoding":"BINARY","IsDracoCompressed":false,"AutomationTag":null,"CountTriangles":149999,"idModel":1},"ModelObjects":[{"idModelObject":1,"idModel":1,"BoundingBoxP1X":-892.2620849609375,"BoundingBoxP1Y":-2167.86767578125,"BoundingBoxP1Z":-971.3925170898438,"BoundingBoxP2X":892.2653198242188,"BoundingBoxP2Y":2167.867919921875,"BoundingBoxP2Z":971.3912963867188,"CountVertices":74796,"CountFaces":149999,"CountColorChannels":0,"CountTextureCoordinateChannels":1,"HasBones":false,"HasFaceNormals":true,"HasTangents":null,"HasTextureCoordinates":true,"HasVertexNormals":null,"HasVertexColor":false,"IsTwoManifoldUnbounded":false,"IsTwoManifoldBounded":false,"IsWatertight":false,"SelfIntersecting":true,"CountTriangles":149999}],"ModelMaterials":[{"idModelMaterial":1,"Name":"material_0"}],"ModelMaterialChannels":[{"idModelMaterialChannel":1,"idModelMaterial":1,"idVMaterialType":64,"MaterialTypeOther":null,"idModelMaterialUVMap":1,"UVMapEmbedded":false,"ChannelPosition":0,"ChannelWidth":3,"Scalar1":0.8,"Scalar2":0.8,"Scalar3":0.8,"Scalar4":null,"AdditionalAttributes":null,"Source":"/Users/blundellj/OneDrive - Smithsonian Institution/Packrat demo files/model validation demo files/eremotherium_laurillardi-150k-4096-obj/eremotherium_laurillardi-150k-4096-diffuse.jpg"}],"ModelMaterialUVMaps":[{"idModel":1,"idAsset":2,"UVMapEdgeLength":0,"idModelMaterialUVMap":1}],"ModelObjectModelMaterialXref":[{"idModelObjectModelMaterialXref":1,"idModelObject":1,"idModelMaterial":1}],"ModelAssets":[{"Asset":{"FileName":"eremotherium_laurillardi-150k-4096.fbx","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":1},"AssetVersion":{"idAsset":1,"Version":0,"FileName":"eremotherium_laurillardi-150k-4096.fbx","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":1,"IngestedOrig":null},"AssetName":"eremotherium_laurillardi-150k-4096.fbx","AssetType":"Model"},{"Asset":{"FileName":"/Users/blundellj/OneDrive - Smithsonian Institution/Packrat demo files/model validation demo files/eremotherium_laurillardi-150k-4096-obj/eremotherium_laurillardi-150k-4096-diffuse.jpg","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":2},"AssetVersion":{"idAsset":2,"Version":0,"FileName":"/Users/blundellj/OneDrive - Smithsonian Institution/Packrat demo files/model validation demo files/eremotherium_laurillardi-150k-4096-obj/eremotherium_laurillardi-150k-4096-diffuse.jpg","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":2,"IngestedOrig":null},"AssetName":"/Users/blundellj/OneDrive - Smithsonian Institution/Packrat demo files/model validation demo files/eremotherium_laurillardi-150k-4096-obj/eremotherium_laurillardi-150k-4096-diffuse.jpg","AssetType":"Texture Map diffuse"}]}}'], + ['glb', '{"success":true,"error":"","modelConstellation":{"Model":{"Name":"eremotherium_laurillardi-150k-4096.glb","Title":"","DateCreated":"2021-04-01T00:00:00.000Z","idVCreationMethod":0,"idVModality":0,"idVUnits":0,"idVPurpose":0,"idVFileType":52,"idAssetThumbnail":null,"CountAnimations":0,"CountCameras":0,"CountFaces":149999,"CountLights":0,"CountMaterials":1,"CountMeshes":1,"CountVertices":104561,"CountEmbeddedTextures":1,"CountLinkedTextures":0,"FileEncoding":"BINARY","IsDracoCompressed":false,"AutomationTag":null,"CountTriangles":149999,"idModel":1},"ModelObjects":[{"idModelObject":1,"idModel":1,"BoundingBoxP1X":-892.2620849609375,"BoundingBoxP1Y":-2167.867431640625,"BoundingBoxP1Z":-971.3921508789062,"BoundingBoxP2X":892.2653198242188,"BoundingBoxP2Y":2167.86767578125,"BoundingBoxP2Z":971.3909301757812,"CountVertices":104561,"CountFaces":149999,"CountColorChannels":0,"CountTextureCoordinateChannels":1,"HasBones":false,"HasFaceNormals":true,"HasTangents":null,"HasTextureCoordinates":true,"HasVertexNormals":null,"HasVertexColor":false,"IsTwoManifoldUnbounded":false,"IsTwoManifoldBounded":true,"IsWatertight":false,"SelfIntersecting":true,"CountTriangles":149999}],"ModelMaterials":[{"idModelMaterial":1,"Name":"material_0"}],"ModelMaterialChannels":[{"idModelMaterialChannel":1,"idModelMaterial":1,"idVMaterialType":64,"MaterialTypeOther":null,"idModelMaterialUVMap":null,"UVMapEmbedded":true,"ChannelPosition":null,"ChannelWidth":null,"Scalar1":1,"Scalar2":1,"Scalar3":1,"Scalar4":1,"AdditionalAttributes":null}],"ModelMaterialUVMaps":null,"ModelObjectModelMaterialXref":[{"idModelObjectModelMaterialXref":1,"idModelObject":1,"idModelMaterial":1}],"ModelAssets":[{"Asset":{"FileName":"eremotherium_laurillardi-150k-4096.glb","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":1},"AssetVersion":{"idAsset":1,"Version":0,"FileName":"eremotherium_laurillardi-150k-4096.glb","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":1,"IngestedOrig":null},"AssetName":"eremotherium_laurillardi-150k-4096.glb","AssetType":"Model"}]}}'], + ['glb-draco', '{"success":true,"error":"","modelConstellation":{"Model":{"Name":"eremotherium_laurillardi-Part-100k-512.glb","Title":"","DateCreated":"2021-04-01T00:00:00.000Z","idVCreationMethod":0,"idVModality":0,"idVUnits":0,"idVPurpose":0,"idVFileType":52,"idAssetThumbnail":null,"CountAnimations":0,"CountCameras":0,"CountFaces":99766,"CountLights":0,"CountMaterials":1,"CountMeshes":1,"CountVertices":64464,"CountEmbeddedTextures":3,"CountLinkedTextures":0,"FileEncoding":"BINARY","IsDracoCompressed":true,"AutomationTag":null,"CountTriangles":99766,"idModel":1},"ModelObjects":[{"idModelObject":1,"idModel":1,"BoundingBoxP1X":-68.70997619628906,"BoundingBoxP1Y":-167.00453186035156,"BoundingBoxP1Z":-61.58316421508789,"BoundingBoxP2X":56.08637237548828,"BoundingBoxP2Y":14.136469841003418,"BoundingBoxP2Z":117.65611267089844,"CountVertices":64464,"CountFaces":99766,"CountColorChannels":0,"CountTextureCoordinateChannels":1,"HasBones":false,"HasFaceNormals":true,"HasTangents":null,"HasTextureCoordinates":true,"HasVertexNormals":null,"HasVertexColor":false,"IsTwoManifoldUnbounded":false,"IsTwoManifoldBounded":true,"IsWatertight":false,"SelfIntersecting":true,"CountTriangles":99766}],"ModelMaterials":[{"idModelMaterial":1,"Name":"default"}],"ModelMaterialChannels":[{"idModelMaterialChannel":1,"idModelMaterial":1,"idVMaterialType":64,"MaterialTypeOther":null,"idModelMaterialUVMap":null,"UVMapEmbedded":true,"ChannelPosition":null,"ChannelWidth":null,"Scalar1":1,"Scalar2":1,"Scalar3":1,"Scalar4":1,"AdditionalAttributes":null},{"idModelMaterialChannel":2,"idModelMaterial":1,"idVMaterialType":69,"MaterialTypeOther":null,"idModelMaterialUVMap":null,"UVMapEmbedded":true,"ChannelPosition":null,"ChannelWidth":null,"Scalar1":null,"Scalar2":null,"Scalar3":null,"Scalar4":null,"AdditionalAttributes":null},{"idModelMaterialChannel":3,"idModelMaterial":1,"idVMaterialType":73,"MaterialTypeOther":null,"idModelMaterialUVMap":null,"UVMapEmbedded":true,"ChannelPosition":null,"ChannelWidth":null,"Scalar1":null,"Scalar2":null,"Scalar3":null,"Scalar4":null,"AdditionalAttributes":null}],"ModelMaterialUVMaps":null,"ModelObjectModelMaterialXref":[{"idModelObjectModelMaterialXref":1,"idModelObject":1,"idModelMaterial":1}],"ModelAssets":[{"Asset":{"FileName":"eremotherium_laurillardi-Part-100k-512.glb","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":1},"AssetVersion":{"idAsset":1,"Version":0,"FileName":"eremotherium_laurillardi-Part-100k-512.glb","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":1,"IngestedOrig":null},"AssetName":"eremotherium_laurillardi-Part-100k-512.glb","AssetType":"Model"}]}}'], + ['obj', '{"success":true,"error":"","modelConstellation":{"Model":{"Name":"eremotherium_laurillardi-150k-4096.obj","Title":"","DateCreated":"2021-04-01T00:00:00.000Z","idVCreationMethod":0,"idVModality":0,"idVUnits":0,"idVPurpose":0,"idVFileType":49,"idAssetThumbnail":null,"CountAnimations":0,"CountCameras":0,"CountFaces":149999,"CountLights":0,"CountMaterials":1,"CountMeshes":1,"CountVertices":74796,"CountEmbeddedTextures":0,"CountLinkedTextures":1,"FileEncoding":"ASCII","IsDracoCompressed":false,"AutomationTag":null,"CountTriangles":149999,"idModel":1},"ModelObjects":[{"idModelObject":1,"idModel":1,"BoundingBoxP1X":-892.2620849609375,"BoundingBoxP1Y":-971.3923950195312,"BoundingBoxP1Z":-2167.867919921875,"BoundingBoxP2X":892.2653198242188,"BoundingBoxP2Y":971.3911743164062,"BoundingBoxP2Z":2167.86767578125,"CountVertices":74796,"CountFaces":149999,"CountColorChannels":0,"CountTextureCoordinateChannels":1,"HasBones":false,"HasFaceNormals":true,"HasTangents":null,"HasTextureCoordinates":true,"HasVertexNormals":null,"HasVertexColor":false,"IsTwoManifoldUnbounded":false,"IsTwoManifoldBounded":false,"IsWatertight":false,"SelfIntersecting":true,"CountTriangles":149999}],"ModelMaterials":[{"idModelMaterial":1,"Name":"material_0"}],"ModelMaterialChannels":[{"idModelMaterialChannel":1,"idModelMaterial":1,"idVMaterialType":64,"MaterialTypeOther":null,"idModelMaterialUVMap":1,"UVMapEmbedded":false,"ChannelPosition":0,"ChannelWidth":3,"Scalar1":0.6,"Scalar2":0.6,"Scalar3":0.6,"Scalar4":null,"AdditionalAttributes":null,"Source":"eremotherium_laurillardi-150k-4096-diffuse.jpg"}],"ModelMaterialUVMaps":[{"idModel":1,"idAsset":2,"UVMapEdgeLength":0,"idModelMaterialUVMap":1}],"ModelObjectModelMaterialXref":[{"idModelObjectModelMaterialXref":1,"idModelObject":1,"idModelMaterial":1}],"ModelAssets":[{"Asset":{"FileName":"eremotherium_laurillardi-150k-4096.obj","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":1},"AssetVersion":{"idAsset":1,"Version":0,"FileName":"eremotherium_laurillardi-150k-4096.obj","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":1,"IngestedOrig":null},"AssetName":"eremotherium_laurillardi-150k-4096.obj","AssetType":"Model"},{"Asset":{"FileName":"eremotherium_laurillardi-150k-4096-diffuse.jpg","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":2},"AssetVersion":{"idAsset":2,"Version":0,"FileName":"eremotherium_laurillardi-150k-4096-diffuse.jpg","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":2,"IngestedOrig":null},"AssetName":"eremotherium_laurillardi-150k-4096-diffuse.jpg","AssetType":"Texture Map diffuse"}]}}'], + ['ply', '{"success":true,"error":"","modelConstellation":{"Model":{"Name":"eremotherium_laurillardi-150k.ply","Title":"","DateCreated":"2021-04-01T00:00:00.000Z","idVCreationMethod":0,"idVModality":0,"idVUnits":0,"idVPurpose":0,"idVFileType":50,"idAssetThumbnail":null,"CountAnimations":0,"CountCameras":0,"CountFaces":149999,"CountLights":0,"CountMaterials":1,"CountMeshes":1,"CountVertices":74796,"CountEmbeddedTextures":0,"CountLinkedTextures":1,"FileEncoding":"BINARY","IsDracoCompressed":false,"AutomationTag":null,"CountTriangles":149999,"idModel":1},"ModelObjects":[{"idModelObject":1,"idModel":1,"BoundingBoxP1X":-892.2620849609375,"BoundingBoxP1Y":-971.3921508789062,"BoundingBoxP1Z":-2167.86767578125,"BoundingBoxP2X":892.2653198242188,"BoundingBoxP2Y":971.3909301757812,"BoundingBoxP2Z":2167.867431640625,"CountVertices":74796,"CountFaces":149999,"CountColorChannels":1,"CountTextureCoordinateChannels":0,"HasBones":false,"HasFaceNormals":false,"HasTangents":null,"HasTextureCoordinates":false,"HasVertexNormals":null,"HasVertexColor":true,"IsTwoManifoldUnbounded":false,"IsTwoManifoldBounded":false,"IsWatertight":false,"SelfIntersecting":true,"CountTriangles":149999}],"ModelMaterials":[{"idModelMaterial":1,"Name":""}],"ModelMaterialChannels":[{"idModelMaterialChannel":1,"idModelMaterial":1,"idVMaterialType":64,"MaterialTypeOther":null,"idModelMaterialUVMap":1,"UVMapEmbedded":false,"ChannelPosition":0,"ChannelWidth":3,"Scalar1":1,"Scalar2":1,"Scalar3":1,"Scalar4":1,"AdditionalAttributes":null,"Source":"eremotherium_laurillardi-150k-4096-diffuse.jpg"}],"ModelMaterialUVMaps":[{"idModel":1,"idAsset":2,"UVMapEdgeLength":0,"idModelMaterialUVMap":1}],"ModelObjectModelMaterialXref":[{"idModelObjectModelMaterialXref":1,"idModelObject":1,"idModelMaterial":1}],"ModelAssets":[{"Asset":{"FileName":"eremotherium_laurillardi-150k.ply","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":1},"AssetVersion":{"idAsset":1,"Version":0,"FileName":"eremotherium_laurillardi-150k.ply","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":1,"IngestedOrig":null},"AssetName":"eremotherium_laurillardi-150k.ply","AssetType":"Model"},{"Asset":{"FileName":"eremotherium_laurillardi-150k-4096-diffuse.jpg","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":2},"AssetVersion":{"idAsset":2,"Version":0,"FileName":"eremotherium_laurillardi-150k-4096-diffuse.jpg","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":2,"IngestedOrig":null},"AssetName":"eremotherium_laurillardi-150k-4096-diffuse.jpg","AssetType":"Texture Map diffuse"}]}}'], + ['stl', '{"success":true,"error":"","modelConstellation":{"Model":{"Name":"eremotherium_laurillardi-150k.stl","Title":"","DateCreated":"2021-04-01T00:00:00.000Z","idVCreationMethod":0,"idVModality":0,"idVUnits":0,"idVPurpose":0,"idVFileType":51,"idAssetThumbnail":null,"CountAnimations":0,"CountCameras":0,"CountFaces":149999,"CountLights":0,"CountMaterials":0,"CountMeshes":1,"CountVertices":74796,"CountEmbeddedTextures":0,"CountLinkedTextures":0,"FileEncoding":"BINARY","IsDracoCompressed":false,"AutomationTag":null,"CountTriangles":149999,"idModel":1},"ModelObjects":[{"idModelObject":1,"idModel":1,"BoundingBoxP1X":-892.2620849609375,"BoundingBoxP1Y":-971.3921508789062,"BoundingBoxP1Z":-2167.86767578125,"BoundingBoxP2X":892.2653198242188,"BoundingBoxP2Y":971.3909301757812,"BoundingBoxP2Z":2167.867431640625,"CountVertices":74796,"CountFaces":149999,"CountColorChannels":0,"CountTextureCoordinateChannels":0,"HasBones":false,"HasFaceNormals":false,"HasTangents":null,"HasTextureCoordinates":false,"HasVertexNormals":null,"HasVertexColor":false,"IsTwoManifoldUnbounded":false,"IsTwoManifoldBounded":false,"IsWatertight":false,"SelfIntersecting":true,"CountTriangles":149999}],"ModelMaterials":null,"ModelMaterialChannels":null,"ModelMaterialUVMaps":null,"ModelObjectModelMaterialXref":null,"ModelAssets":[{"Asset":{"FileName":"eremotherium_laurillardi-150k.stl","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":1},"AssetVersion":{"idAsset":1,"Version":0,"FileName":"eremotherium_laurillardi-150k.stl","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":1,"IngestedOrig":null},"AssetName":"eremotherium_laurillardi-150k.stl","AssetType":"Model"}]}}'], + ['x3d', '{"success":true,"error":"","modelConstellation":{"Model":{"Name":"eremotherium_laurillardi-150k-4096.x3d","Title":"","DateCreated":"2021-04-01T00:00:00.000Z","idVCreationMethod":0,"idVModality":0,"idVUnits":0,"idVPurpose":0,"idVFileType":56,"idAssetThumbnail":null,"CountAnimations":0,"CountCameras":0,"CountFaces":149999,"CountLights":0,"CountMaterials":1,"CountMeshes":1,"CountVertices":74796,"CountEmbeddedTextures":0,"CountLinkedTextures":1,"FileEncoding":"ASCII","IsDracoCompressed":false,"AutomationTag":null,"CountTriangles":149999,"idModel":1},"ModelObjects":[{"idModelObject":1,"idModel":1,"BoundingBoxP1X":-892.2651977539062,"BoundingBoxP1Y":-2167.870361328125,"BoundingBoxP1Z":-971.3923950195312,"BoundingBoxP2X":892.26220703125,"BoundingBoxP2Y":2167.870361328125,"BoundingBoxP2Z":971.391357421875,"CountVertices":74796,"CountFaces":149999,"CountColorChannels":1,"CountTextureCoordinateChannels":1,"HasBones":false,"HasFaceNormals":false,"HasTangents":null,"HasTextureCoordinates":true,"HasVertexNormals":null,"HasVertexColor":true,"IsTwoManifoldUnbounded":false,"IsTwoManifoldBounded":false,"IsWatertight":false,"SelfIntersecting":true,"CountTriangles":149999}],"ModelMaterials":[{"idModelMaterial":1,"Name":""}],"ModelMaterialChannels":[{"idModelMaterialChannel":1,"idModelMaterial":1,"idVMaterialType":64,"MaterialTypeOther":null,"idModelMaterialUVMap":1,"UVMapEmbedded":false,"ChannelPosition":0,"ChannelWidth":3,"Scalar1":null,"Scalar2":null,"Scalar3":null,"Scalar4":null,"AdditionalAttributes":null,"Source":"eremotherium_laurillardi-150k-4096-diffuse.jpg"}],"ModelMaterialUVMaps":[{"idModel":1,"idAsset":2,"UVMapEdgeLength":0,"idModelMaterialUVMap":1}],"ModelObjectModelMaterialXref":null,"ModelAssets":[{"Asset":{"FileName":"eremotherium_laurillardi-150k-4096.x3d","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":1},"AssetVersion":{"idAsset":1,"Version":0,"FileName":"eremotherium_laurillardi-150k-4096.x3d","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":1,"IngestedOrig":null},"AssetName":"eremotherium_laurillardi-150k-4096.x3d","AssetType":"Model"},{"Asset":{"FileName":"eremotherium_laurillardi-150k-4096-diffuse.jpg","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":2},"AssetVersion":{"idAsset":2,"Version":0,"FileName":"eremotherium_laurillardi-150k-4096-diffuse.jpg","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":2,"IngestedOrig":null},"AssetName":"eremotherium_laurillardi-150k-4096-diffuse.jpg","AssetType":"Texture Map diffuse"}]}}'], + ['dae', '{"success":true,"error":"","modelConstellation":{"Model":{"Name":"clemente_helmet.dae","Title":"","DateCreated":"2021-04-01T00:00:00.000Z","idVCreationMethod":0,"idVModality":0,"idVUnits":0,"idVPurpose":0,"idVFileType":58,"idAssetThumbnail":null,"CountAnimations":0,"CountCameras":0,"CountFaces":148260,"CountLights":0,"CountMaterials":1,"CountMeshes":1,"CountVertices":80417,"CountEmbeddedTextures":0,"CountLinkedTextures":1,"FileEncoding":"ASCII","IsDracoCompressed":false,"AutomationTag":null,"CountTriangles":148260,"idModel":1},"ModelObjects":[{"idModelObject":1,"idModel":1,"BoundingBoxP1X":-0.09380000084638596,"BoundingBoxP1Y":-0.13996900618076324,"BoundingBoxP1Z":-0.06898897141218185,"BoundingBoxP2X":0.09380996227264404,"BoundingBoxP2Y":0.139957994222641,"BoundingBoxP2Z":0.0689619705080986,"CountVertices":80417,"CountFaces":148260,"CountColorChannels":0,"CountTextureCoordinateChannels":1,"HasBones":false,"HasFaceNormals":false,"HasTangents":null,"HasTextureCoordinates":true,"HasVertexNormals":null,"HasVertexColor":false,"IsTwoManifoldUnbounded":false,"IsTwoManifoldBounded":true,"IsWatertight":false,"SelfIntersecting":true,"CountTriangles":148260}],"ModelMaterials":[{"idModelMaterial":1,"Name":"default"}],"ModelMaterialChannels":[{"idModelMaterialChannel":1,"idModelMaterial":1,"idVMaterialType":64,"MaterialTypeOther":null,"idModelMaterialUVMap":1,"UVMapEmbedded":false,"ChannelPosition":0,"ChannelWidth":3,"Scalar1":1,"Scalar2":1,"Scalar3":1,"Scalar4":null,"AdditionalAttributes":null,"Source":"Image_0.jpg"}],"ModelMaterialUVMaps":[{"idModel":1,"idAsset":2,"UVMapEdgeLength":0,"idModelMaterialUVMap":1}],"ModelObjectModelMaterialXref":[{"idModelObjectModelMaterialXref":1,"idModelObject":1,"idModelMaterial":1}],"ModelAssets":[{"Asset":{"FileName":"clemente_helmet.dae","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":1},"AssetVersion":{"idAsset":1,"Version":0,"FileName":"clemente_helmet.dae","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":1,"IngestedOrig":null},"AssetName":"clemente_helmet.dae","AssetType":"Model"},{"Asset":{"FileName":"Image_0.jpg","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":2},"AssetVersion":{"idAsset":2,"Version":0,"FileName":"Image_0.jpg","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":2,"IngestedOrig":null},"AssetName":"Image_0.jpg","AssetType":"Texture Map diffuse"}]}}'], + ['gltf-stand-alone', '{"success":true,"error":"","modelConstellation":{"Model":{"Name":"clemente_helmet.gltf","Title":"","DateCreated":"2021-04-01T00:00:00.000Z","idVCreationMethod":0,"idVModality":0,"idVUnits":0,"idVPurpose":0,"idVFileType":53,"idAssetThumbnail":null,"CountAnimations":0,"CountCameras":0,"CountFaces":148260,"CountLights":0,"CountMaterials":1,"CountMeshes":1,"CountVertices":80417,"CountEmbeddedTextures":3,"CountLinkedTextures":0,"FileEncoding":"ASCII","IsDracoCompressed":false,"AutomationTag":null,"CountTriangles":148260,"idModel":1},"ModelObjects":[{"idModelObject":1,"idModel":1,"BoundingBoxP1X":-0.09380000084638596,"BoundingBoxP1Y":-0.13996900618076324,"BoundingBoxP1Z":-0.06898900121450424,"BoundingBoxP2X":0.09380999952554703,"BoundingBoxP2Y":0.139957994222641,"BoundingBoxP2Z":0.06896200031042099,"CountVertices":80417,"CountFaces":148260,"CountColorChannels":0,"CountTextureCoordinateChannels":1,"HasBones":false,"HasFaceNormals":true,"HasTangents":null,"HasTextureCoordinates":true,"HasVertexNormals":null,"HasVertexColor":false,"IsTwoManifoldUnbounded":false,"IsTwoManifoldBounded":true,"IsWatertight":false,"SelfIntersecting":true,"CountTriangles":148260}],"ModelMaterials":[{"idModelMaterial":1,"Name":"default"}],"ModelMaterialChannels":[{"idModelMaterialChannel":1,"idModelMaterial":1,"idVMaterialType":64,"MaterialTypeOther":null,"idModelMaterialUVMap":null,"UVMapEmbedded":true,"ChannelPosition":null,"ChannelWidth":null,"Scalar1":1,"Scalar2":1,"Scalar3":1,"Scalar4":1,"AdditionalAttributes":null},{"idModelMaterialChannel":2,"idModelMaterial":1,"idVMaterialType":69,"MaterialTypeOther":null,"idModelMaterialUVMap":null,"UVMapEmbedded":true,"ChannelPosition":null,"ChannelWidth":null,"Scalar1":null,"Scalar2":null,"Scalar3":null,"Scalar4":null,"AdditionalAttributes":null},{"idModelMaterialChannel":3,"idModelMaterial":1,"idVMaterialType":73,"MaterialTypeOther":null,"idModelMaterialUVMap":null,"UVMapEmbedded":true,"ChannelPosition":null,"ChannelWidth":null,"Scalar1":null,"Scalar2":null,"Scalar3":null,"Scalar4":null,"AdditionalAttributes":null}],"ModelMaterialUVMaps":null,"ModelObjectModelMaterialXref":[{"idModelObjectModelMaterialXref":1,"idModelObject":1,"idModelMaterial":1}],"ModelAssets":[{"Asset":{"FileName":"clemente_helmet.gltf","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":1},"AssetVersion":{"idAsset":1,"Version":0,"FileName":"clemente_helmet.gltf","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":1,"IngestedOrig":null},"AssetName":"clemente_helmet.gltf","AssetType":"Model"}]}}'], + ['gltf-with-support', '{"success":true,"error":"","modelConstellation":{"Model":{"Name":"nmah-1981_0706_06-clemente_helmet-150k-4096.gltf","Title":"","DateCreated":"2021-04-01T00:00:00.000Z","idVCreationMethod":0,"idVModality":0,"idVUnits":0,"idVPurpose":0,"idVFileType":53,"idAssetThumbnail":null,"CountAnimations":0,"CountCameras":0,"CountFaces":148260,"CountLights":0,"CountMaterials":1,"CountMeshes":1,"CountVertices":80417,"CountEmbeddedTextures":0,"CountLinkedTextures":3,"FileEncoding":"ASCII","IsDracoCompressed":false,"AutomationTag":null,"CountTriangles":148260,"idModel":1},"ModelObjects":[{"idModelObject":1,"idModel":1,"BoundingBoxP1X":-0.09380000084638596,"BoundingBoxP1Y":-0.13996900618076324,"BoundingBoxP1Z":-0.06898900121450424,"BoundingBoxP2X":0.09380999952554703,"BoundingBoxP2Y":0.139957994222641,"BoundingBoxP2Z":0.06896200031042099,"CountVertices":80417,"CountFaces":148260,"CountColorChannels":0,"CountTextureCoordinateChannels":1,"HasBones":false,"HasFaceNormals":true,"HasTangents":null,"HasTextureCoordinates":true,"HasVertexNormals":null,"HasVertexColor":false,"IsTwoManifoldUnbounded":false,"IsTwoManifoldBounded":true,"IsWatertight":false,"SelfIntersecting":true,"CountTriangles":148260}],"ModelMaterials":[{"idModelMaterial":1,"Name":"default"}],"ModelMaterialChannels":[{"idModelMaterialChannel":1,"idModelMaterial":1,"idVMaterialType":64,"MaterialTypeOther":null,"idModelMaterialUVMap":1,"UVMapEmbedded":false,"ChannelPosition":0,"ChannelWidth":3,"Scalar1":1,"Scalar2":1,"Scalar3":1,"Scalar4":1,"AdditionalAttributes":null,"Source":"nmah-1981_0706_06-clemente_helmet-150k-4096-diffuse.jpg"},{"idModelMaterialChannel":2,"idModelMaterial":1,"idVMaterialType":69,"MaterialTypeOther":null,"idModelMaterialUVMap":2,"UVMapEmbedded":false,"ChannelPosition":0,"ChannelWidth":3,"Scalar1":null,"Scalar2":null,"Scalar3":null,"Scalar4":null,"AdditionalAttributes":null,"Source":"nmah-1981_0706_06-clemente_helmet-150k-4096-normals.jpg"},{"idModelMaterialChannel":3,"idModelMaterial":1,"idVMaterialType":73,"MaterialTypeOther":null,"idModelMaterialUVMap":3,"UVMapEmbedded":false,"ChannelPosition":0,"ChannelWidth":3,"Scalar1":null,"Scalar2":null,"Scalar3":null,"Scalar4":null,"AdditionalAttributes":null,"Source":"nmah-1981_0706_06-clemente_helmet-150k-4096-occlusion.jpg"}],"ModelMaterialUVMaps":[{"idModel":1,"idAsset":2,"UVMapEdgeLength":0,"idModelMaterialUVMap":1},{"idModel":1,"idAsset":3,"UVMapEdgeLength":0,"idModelMaterialUVMap":2},{"idModel":1,"idAsset":4,"UVMapEdgeLength":0,"idModelMaterialUVMap":3}],"ModelObjectModelMaterialXref":[{"idModelObjectModelMaterialXref":1,"idModelObject":1,"idModelMaterial":1}],"ModelAssets":[{"Asset":{"FileName":"nmah-1981_0706_06-clemente_helmet-150k-4096.gltf","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":1},"AssetVersion":{"idAsset":1,"Version":0,"FileName":"nmah-1981_0706_06-clemente_helmet-150k-4096.gltf","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":1,"IngestedOrig":null},"AssetName":"nmah-1981_0706_06-clemente_helmet-150k-4096.gltf","AssetType":"Model"},{"Asset":{"FileName":"nmah-1981_0706_06-clemente_helmet-150k-4096-diffuse.jpg","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":2},"AssetVersion":{"idAsset":2,"Version":0,"FileName":"nmah-1981_0706_06-clemente_helmet-150k-4096-diffuse.jpg","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":2,"IngestedOrig":null},"AssetName":"nmah-1981_0706_06-clemente_helmet-150k-4096-diffuse.jpg","AssetType":"Texture Map diffuse"},{"Asset":{"FileName":"nmah-1981_0706_06-clemente_helmet-150k-4096-normals.jpg","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":3},"AssetVersion":{"idAsset":3,"Version":0,"FileName":"nmah-1981_0706_06-clemente_helmet-150k-4096-normals.jpg","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":3,"IngestedOrig":null},"AssetName":"nmah-1981_0706_06-clemente_helmet-150k-4096-normals.jpg","AssetType":"Texture Map normal"},{"Asset":{"FileName":"nmah-1981_0706_06-clemente_helmet-150k-4096-occlusion.jpg","idAssetGroup":null,"idVAssetType":0,"idSystemObject":null,"StorageKey":null,"idAsset":4},"AssetVersion":{"idAsset":4,"Version":0,"FileName":"nmah-1981_0706_06-clemente_helmet-150k-4096-occlusion.jpg","idUserCreator":0,"DateCreated":"2021-04-01T00:00:00.000Z","StorageHash":"","StorageSize":"0","StorageKeyStaging":"","Ingested":null,"BulkIngest":false,"idSOAttachment":null,"FilePath":"","Comment":"Created by Cook si-packrat-inspect","idAssetVersion":4,"IngestedOrig":null},"AssetName":"nmah-1981_0706_06-clemente_helmet-150k-4096-occlusion.jpg","AssetType":"Texture Map occlusion"}]}}'], ]); export class ModelTestSetup { From 675359cd2fa4077c4b9b3020301472c45db443b5 Mon Sep 17 00:00:00 2001 From: Hsin Tung Date: Wed, 9 Mar 2022 11:34:10 -0800 Subject: [PATCH 02/31] addressed ticket 611 --- client/src/constants/helperfunctions.ts | 5 + .../DetailsView/DetailsTab/AssetGrid.tsx | 20 ++- .../DetailsTab/AssetVersionDetails.tsx | 145 +++++++++++------- .../DetailsTab/AssetVersionsTable.tsx | 10 +- .../components/DetailsView/index.tsx | 2 - client/src/store/detailTab.tsx | 4 +- client/src/types/graphql.tsx | 9 +- common/constants.ts | 3 +- .../getDetailsTabDataForObject.ts | 1 + .../systemobject/getVersionsForAsset.ts | 1 + server/graphql/schema.graphql | 3 + .../schema/systemobject/mutations.graphql | 1 + .../schema/systemobject/queries.graphql | 2 + .../resolvers/queries/AssetGridDetail.ts | 5 +- .../queries/AssetGridDetailCaptureData.ts | 3 + .../resolvers/queries/AssetGridDetailScene.ts | 5 +- .../resolvers/queries/getVersionsForAsset.ts | 1 + server/types/graphql.ts | 3 + 18 files changed, 154 insertions(+), 69 deletions(-) diff --git a/client/src/constants/helperfunctions.ts b/client/src/constants/helperfunctions.ts index 466933516..e6cfa342a 100644 --- a/client/src/constants/helperfunctions.ts +++ b/client/src/constants/helperfunctions.ts @@ -134,6 +134,11 @@ export const attachSystemObjectUploadRedirect = (idSystemObject: number, ObjectT export const truncateWithEllipses = (text: string, max: number) => text.slice(0, max - 1) + (text.length > max ? ' ...' : ''); +export const truncateMiddleWithEllipses = (text: string, firstHalf: number, secondHalf: number) => { + if (firstHalf + secondHalf >= text.length) return text; + return text.slice(0, firstHalf) + '...' + text.slice(-secondHalf); +}; + export function getDownloadSiteMapXMLLink(serverEndPoint: string | undefined): string { return `${serverEndPoint}/download/sitemap.xml`; } \ No newline at end of file diff --git a/client/src/pages/Repository/components/DetailsView/DetailsTab/AssetGrid.tsx b/client/src/pages/Repository/components/DetailsView/DetailsTab/AssetGrid.tsx index a6414e5fe..039dc52cd 100644 --- a/client/src/pages/Repository/components/DetailsView/DetailsTab/AssetGrid.tsx +++ b/client/src/pages/Repository/components/DetailsView/DetailsTab/AssetGrid.tsx @@ -26,6 +26,7 @@ import { createMuiTheme, MuiThemeProvider } from '@material-ui/core/styles'; import clsx from 'clsx'; import { DataTableOptions } from '../../../../../types/component'; import API from '../../../../../api'; +import { truncateMiddleWithEllipses } from '../../../../../constants'; export const useStyles = makeStyles(({ palette }) => ({ btn: { @@ -73,7 +74,7 @@ export const useStyles = makeStyles(({ palette }) => ({ paddingBottom: 5 }, date: { - fontSize: '0.9em', + fontSize: '0.8rem', color: palette.primary.dark }, empty: { @@ -302,8 +303,22 @@ function AssetGrid(props: AssetGridProps): React.ReactElement { ); } }; + break; + case eAssetGridColumnType.eTruncate: + gridColumnObject.options = { + ...gridColumnObject.options, + customBodyRender(value) { + return ( + }> + + {truncateMiddleWithEllipses(value, 6, 7)} + + + ); + } + } + break; } - result.push(gridColumnObject); }); return result; @@ -351,7 +366,6 @@ function AssetGrid(props: AssetGridProps): React.ReactElement { fixedHeader: false, pagination: false, elevation: 0, - viewColumns: false, onViewColumnsChange: toggleColumn }; diff --git a/client/src/pages/Repository/components/DetailsView/DetailsTab/AssetVersionDetails.tsx b/client/src/pages/Repository/components/DetailsView/DetailsTab/AssetVersionDetails.tsx index a630fac7c..221e64e2c 100644 --- a/client/src/pages/Repository/components/DetailsView/DetailsTab/AssetVersionDetails.tsx +++ b/client/src/pages/Repository/components/DetailsView/DetailsTab/AssetVersionDetails.tsx @@ -5,10 +5,10 @@ * * This component renders details tab for AssetVersion specific details used in DetailsTab component. */ -import { Box, makeStyles, Typography, Button } from '@material-ui/core'; +import { Box, makeStyles, Typography, Button, Table, TableBody, TableCell, TableContainer, TableRow, Checkbox } from '@material-ui/core'; import React, { useEffect } from 'react'; import { useHistory } from 'react-router-dom'; -import { CheckboxField, InputField, FieldType, Loader } from '../../../../../components'; +import { Loader } from '../../../../../components'; import { isFieldUpdated } from '../../../../../utils/repository'; import { formatBytes } from '../../../../../utils/upload'; import { DetailComponentProps } from './index'; @@ -18,8 +18,10 @@ import { eSystemObjectType } from '@dpo-packrat/common'; import { apolloClient } from '../../../../../graphql'; import { GetAssetDocument } from '../../../../../types/graphql'; import { useDetailTabStore } from '../../../../../store'; +import { DebounceInput } from 'react-debounce-input'; +import { useStyles, updatedFieldStyling } from './CaptureDataDetails'; -export const useStyles = makeStyles(() => ({ +export const useAVDetailsStyles = makeStyles(() => ({ value: { fontSize: '0.8em', color: 'black', @@ -31,9 +33,9 @@ export const useStyles = makeStyles(() => ({ })); function AssetVersionDetails(props: DetailComponentProps): React.ReactElement { - const classes = useStyles(); + const AVclasses = useAVDetailsStyles(); + const tableClasses = useStyles(); const { data, loading, onUpdateDetail, objectType } = props; - const { disabled } = props; const history = useHistory(); const [AssetVersionDetails, updateDetailField] = useDetailTabStore(state => [state.AssetVersionDetails, state.updateDetailField]); @@ -50,15 +52,7 @@ function AssetVersionDetails(props: DetailComponentProps): React.ReactElement { updateDetailField(eSystemObjectType.eAssetVersion, name, value); }; - const setCheckboxField = ({ target }): void => { - const { name, checked } = target; - updateDetailField(eSystemObjectType.eAssetVersion, name, checked); - }; - - const rowFieldProps = { alignItems: 'center', style: { borderRadius: 0 } }; - const assetVersionData = data.getDetailsTabDataForObject?.AssetVersion; - let redirect = () => {}; if (assetVersionData) { // redirect function fetches assetType so that uploads remembers the assetType for uploads @@ -80,49 +74,88 @@ function AssetVersionDetails(props: DetailComponentProps): React.ReactElement { return ( - - - {AssetVersionDetails.Version} - - - - {AssetVersionDetails.Creator} - - - {AssetVersionDetails.DateCreated} - - - {formatBytes(AssetVersionDetails.StorageSize ?? 0)} - - - - - diff --git a/client/src/pages/Repository/components/DetailsView/DetailsTab/AssetVersionsTable.tsx b/client/src/pages/Repository/components/DetailsView/DetailsTab/AssetVersionsTable.tsx index f32651048..6bf36b879 100644 --- a/client/src/pages/Repository/components/DetailsView/DetailsTab/AssetVersionsTable.tsx +++ b/client/src/pages/Repository/components/DetailsView/DetailsTab/AssetVersionsTable.tsx @@ -17,7 +17,7 @@ import { useObjectVersions, rollbackAssetVersion } from '../../../hooks/useDetai import { useStyles } from './AssetGrid'; import GetAppIcon from '@material-ui/icons/GetApp'; import API from '../../../../../api'; -import { truncateWithEllipses } from '../../../../../constants/helperfunctions'; +import { truncateWithEllipses, truncateMiddleWithEllipses } from '../../../../../constants/helperfunctions'; import { MdExpandMore, MdExpandLess } from 'react-icons/md'; import { toast } from 'react-toastify'; @@ -61,6 +61,9 @@ function AssetVersionsTable(props: AssetVersionsTableProps): React.ReactElement }, { name: 'Date Created', width: '100px' + }, { + name: 'Hash', + width: '120px' }, { name: 'Size', width: '70px' @@ -165,6 +168,11 @@ function AssetVersionsTable(props: AssetVersionsTableProps): React.ReactElement {formatDate(version.dateCreated)} + + }> + {truncateMiddleWithEllipses(version.hash, 6, 7)} + + {formatBytes(version.size)} diff --git a/client/src/pages/Repository/components/DetailsView/index.tsx b/client/src/pages/Repository/components/DetailsView/index.tsx index 8570a6c3c..15df1381c 100644 --- a/client/src/pages/Repository/components/DetailsView/index.tsx +++ b/client/src/pages/Repository/components/DetailsView/index.tsx @@ -144,7 +144,6 @@ function DetailsView(): React.ReactElement { useEffect(() => { if (data && !loading) { const { name, retired, license, metadata } = data.getSystemObjectDetails; - console.log('metadata', metadata); setDetails({ name, retired, idLicense: license?.idLicense || 0 }); initializeIdentifierState(data.getSystemObjectDetails.identifiers); if (objectType === eSystemObjectType.eSubject) { @@ -455,7 +454,6 @@ function DetailsView(): React.ReactElement { } const metadata = getAllMetadataEntries().filter(entry => entry.Name); - // console.log('metadata', metadata); updatedData.Metadata = metadata; const { data } = await updateDetailsTabData(idSystemObject, idObject, objectType, updatedData); diff --git a/client/src/store/detailTab.tsx b/client/src/store/detailTab.tsx index 8b4779c8c..37ec6aa59 100644 --- a/client/src/store/detailTab.tsx +++ b/client/src/store/detailTab.tsx @@ -505,13 +505,15 @@ export const useDetailTabStore = create((set: SetState; Version?: Maybe; StorageSize?: Maybe; + StorageHash?: Maybe; }; export type ActorDetailFieldsInput = { @@ -1874,6 +1875,7 @@ export type AssetVersionDetailFields = { AssetVersion?: Maybe; idAsset?: Maybe; idAssetVersion?: Maybe; + StorageHash?: Maybe; }; export type ActorDetailFields = { @@ -1989,6 +1991,7 @@ export type DetailVersion = { creator: Scalars['String']; dateCreated: Scalars['DateTime']; size: Scalars['BigInt']; + hash: Scalars['String']; ingested: Scalars['Boolean']; Comment?: Maybe; CommentLink?: Maybe; @@ -3648,7 +3651,7 @@ export type GetDetailsTabDataForObjectQuery = ( & Pick )>, AssetVersion?: Maybe<( { __typename?: 'AssetVersionDetailFields' } - & Pick + & Pick )>, Actor?: Maybe<( { __typename?: 'ActorDetailFields' } & Pick @@ -3775,7 +3778,7 @@ export type GetVersionsForAssetQuery = ( { __typename?: 'GetVersionsForAssetResult' } & { versions: Array<( { __typename?: 'DetailVersion' } - & Pick + & Pick )> } ) } ); @@ -6431,6 +6434,7 @@ export const GetDetailsTabDataForObjectDocument = gql` idAsset idAssetVersion FilePath + StorageHash } Actor { OrganizationName @@ -6725,6 +6729,7 @@ export const GetVersionsForAssetDocument = gql` creator dateCreated size + hash ingested Comment CommentLink diff --git a/common/constants.ts b/common/constants.ts index 638eca06e..ecc449e65 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -331,7 +331,8 @@ export enum eAssetGridColumnType { eBoolean = 2, eHyperLink = 3, eDate = 4, - eFileSize = 5 + eFileSize = 5, + eTruncate = 6 } // Keep this in sync with SQL in WorkflowListResult.search() diff --git a/server/graphql/api/queries/systemobject/getDetailsTabDataForObject.ts b/server/graphql/api/queries/systemobject/getDetailsTabDataForObject.ts index 1de1e0061..6d229820f 100644 --- a/server/graphql/api/queries/systemobject/getDetailsTabDataForObject.ts +++ b/server/graphql/api/queries/systemobject/getDetailsTabDataForObject.ts @@ -158,6 +158,7 @@ const getDetailsTabDataForObject = gql` idAsset idAssetVersion FilePath + StorageHash } Actor { OrganizationName diff --git a/server/graphql/api/queries/systemobject/getVersionsForAsset.ts b/server/graphql/api/queries/systemobject/getVersionsForAsset.ts index f618fb8bc..f81fd31ff 100644 --- a/server/graphql/api/queries/systemobject/getVersionsForAsset.ts +++ b/server/graphql/api/queries/systemobject/getVersionsForAsset.ts @@ -11,6 +11,7 @@ const getVersionsForAsset = gql` creator dateCreated size + hash ingested Comment CommentLink diff --git a/server/graphql/schema.graphql b/server/graphql/schema.graphql index c2cb2543d..23a64038a 100644 --- a/server/graphql/schema.graphql +++ b/server/graphql/schema.graphql @@ -1142,6 +1142,7 @@ input AssetVersionDetailFieldsInput { Ingested: Boolean Version: Int StorageSize: BigInt + StorageHash: String } input ActorDetailFieldsInput { @@ -1406,6 +1407,7 @@ type AssetVersionDetailFields { AssetVersion: AssetVersion idAsset: Int idAssetVersion: Int + StorageHash: String } type ActorDetailFields { @@ -1511,6 +1513,7 @@ type DetailVersion { creator: String! dateCreated: DateTime! size: BigInt! + hash: String! ingested: Boolean! Comment: String CommentLink: String diff --git a/server/graphql/schema/systemobject/mutations.graphql b/server/graphql/schema/systemobject/mutations.graphql index 2af940d04..2fe152506 100644 --- a/server/graphql/schema/systemobject/mutations.graphql +++ b/server/graphql/schema/systemobject/mutations.graphql @@ -108,6 +108,7 @@ input AssetVersionDetailFieldsInput { Ingested: Boolean Version: Int StorageSize: BigInt + StorageHash: String } input ActorDetailFieldsInput { diff --git a/server/graphql/schema/systemobject/queries.graphql b/server/graphql/schema/systemobject/queries.graphql index 9d144bcb5..d5e508e6f 100644 --- a/server/graphql/schema/systemobject/queries.graphql +++ b/server/graphql/schema/systemobject/queries.graphql @@ -117,6 +117,7 @@ type AssetVersionDetailFields { AssetVersion: AssetVersion idAsset: Int idAssetVersion: Int + StorageHash: String } type ActorDetailFields { @@ -222,6 +223,7 @@ type DetailVersion { creator: String! dateCreated: DateTime! size: BigInt! + hash: String! ingested: Boolean! Comment: String CommentLink: String diff --git a/server/graphql/schema/systemobject/resolvers/queries/AssetGridDetail.ts b/server/graphql/schema/systemobject/resolvers/queries/AssetGridDetail.ts index 86a11b11c..be28cc9f4 100644 --- a/server/graphql/schema/systemobject/resolvers/queries/AssetGridDetail.ts +++ b/server/graphql/schema/systemobject/resolvers/queries/AssetGridDetail.ts @@ -12,6 +12,7 @@ export class AssetGridDetail extends AssetGridDetailBase { assetType: string; version: number; dateCreated: Date; + hash: string; size: string; constructor(_asset: DBAPI.Asset, assetVersion: DBAPI.AssetVersion, idSystemObject: number, vocabulary: DBAPI.Vocabulary) { @@ -22,6 +23,7 @@ export class AssetGridDetail extends AssetGridDetailBase { this.assetType = vocabulary.Term; this.version = assetVersion.Version; this.dateCreated = assetVersion.DateCreated; + this.hash = assetVersion.StorageHash; this.size = assetVersion.StorageSize.toString(); } @@ -33,7 +35,8 @@ export class AssetGridDetail extends AssetGridDetailBase { { colName: 'assetType', colLabel: 'Asset Type', colDisplay: true, colType: COMMON.eAssetGridColumnType.eString, colAlign: 'center' }, { colName: 'version', colLabel: 'Version', colDisplay: true, colType: COMMON.eAssetGridColumnType.eNumber, colAlign: 'center' }, { colName: 'dateCreated', colLabel: 'Date Created', colDisplay: true, colType: COMMON.eAssetGridColumnType.eDate, colAlign: 'center' }, - { colName: 'size', colLabel: 'Size', colDisplay: true, colType: COMMON.eAssetGridColumnType.eFileSize, colAlign: 'center' } + { colName: 'hash', colLabel: 'Hash', colDisplay: true, colType: COMMON.eAssetGridColumnType.eTruncate, colAlign: 'center' }, + { colName: 'size', colLabel: 'Size', colDisplay: true, colType: COMMON.eAssetGridColumnType.eFileSize, colAlign: 'center' }, ]; } } diff --git a/server/graphql/schema/systemobject/resolvers/queries/AssetGridDetailCaptureData.ts b/server/graphql/schema/systemobject/resolvers/queries/AssetGridDetailCaptureData.ts index a928461fc..b43f0abdb 100644 --- a/server/graphql/schema/systemobject/resolvers/queries/AssetGridDetailCaptureData.ts +++ b/server/graphql/schema/systemobject/resolvers/queries/AssetGridDetailCaptureData.ts @@ -11,6 +11,7 @@ export class AssetGridDetailCaptureData extends AssetGridDetailBase { name: LinkObject; variant: string | null; version: number; + hash: string; size: string; dateCreated: Date; iso: number | null; @@ -25,6 +26,7 @@ export class AssetGridDetailCaptureData extends AssetGridDetailBase { this.name = { label: assetVersion.FileName, path: `${RouteBuilder.RepositoryDetails(idSystemObject)}`, icon: null, origin: COMMON.eLinkOrigin.eClient }; this.variant = H.Helpers.safeString(metadataMap.get('variant')); this.version = assetVersion.Version; + this.hash = assetVersion.StorageHash; this.size = assetVersion.StorageSize.toString(); this.dateCreated = assetVersion.DateCreated; @@ -40,6 +42,7 @@ export class AssetGridDetailCaptureData extends AssetGridDetailBase { { colName: 'link', colLabel: 'Link', colDisplay: true, colType: COMMON.eAssetGridColumnType.eHyperLink, colAlign: 'center' }, { colName: 'name', colLabel: 'Name', colDisplay: true, colType: COMMON.eAssetGridColumnType.eHyperLink, colAlign: 'left' }, { colName: 'variant', colLabel: 'Variant', colDisplay: true, colType: COMMON.eAssetGridColumnType.eString, colAlign: 'center' }, + { colName: 'hash', colLabel: 'Hash', colDisplay: true, colType: COMMON.eAssetGridColumnType.eTruncate, colAlign: 'left' }, { colName: 'size', colLabel: 'Size', colDisplay: true, colType: COMMON.eAssetGridColumnType.eFileSize, colAlign: 'left' }, { colName: 'imageHeight', colLabel: 'Height', colDisplay: true, colType: COMMON.eAssetGridColumnType.eNumber, colAlign: 'center' }, { colName: 'imageWidth', colLabel: 'Width', colDisplay: true, colType: COMMON.eAssetGridColumnType.eNumber, colAlign: 'center' }, diff --git a/server/graphql/schema/systemobject/resolvers/queries/AssetGridDetailScene.ts b/server/graphql/schema/systemobject/resolvers/queries/AssetGridDetailScene.ts index c1dd6e07a..6dd73f809 100644 --- a/server/graphql/schema/systemobject/resolvers/queries/AssetGridDetailScene.ts +++ b/server/graphql/schema/systemobject/resolvers/queries/AssetGridDetailScene.ts @@ -15,6 +15,7 @@ export class AssetGridDetailScene extends AssetGridDetailBase { version: number; dateCreated: Date; size: string; + hash: string; // usage: string | null; quality: string | null; @@ -38,8 +39,8 @@ export class AssetGridDetailScene extends AssetGridDetailBase { this.assetType = vocabulary.Term; this.version = assetVersion.Version; this.dateCreated = assetVersion.DateCreated; + this.hash = assetVersion.StorageHash; this.size = assetVersion.StorageSize.toString(); - // this.usage = H.Helpers.safeString(metadataMap.get('usage')); this.quality = H.Helpers.safeString(metadataMap.get('quality')); this.uvResolution = H.Helpers.safeNumber(metadataMap.get('uvresolution')); @@ -64,8 +65,8 @@ export class AssetGridDetailScene extends AssetGridDetailBase { { colName: 'assetType', colLabel: 'Asset Type', colDisplay: true, colType: COMMON.eAssetGridColumnType.eString, colAlign: 'center' }, { colName: 'version', colLabel: 'Version', colDisplay: true, colType: COMMON.eAssetGridColumnType.eNumber, colAlign: 'center' }, { colName: 'dateCreated', colLabel: 'Date Created', colDisplay: true, colType: COMMON.eAssetGridColumnType.eDate, colAlign: 'center' }, + { colName: 'hash', colLabel: 'Hash', colDisplay: true, colType: COMMON.eAssetGridColumnType.eTruncate, colAlign: 'right' }, { colName: 'size', colLabel: 'Size', colDisplay: true, colType: COMMON.eAssetGridColumnType.eFileSize, colAlign: 'right' }, - // { colName: 'usage', colLabel: 'Usage', colDisplay: true, colType: COMMON.eAssetGridColumnType.eString, colAlign: 'left' }, { colName: 'quality', colLabel: 'Quality', colDisplay: true, colType: COMMON.eAssetGridColumnType.eString, colAlign: 'center' }, { colName: 'uvResolution', colLabel: 'UV', colDisplay: true, colType: COMMON.eAssetGridColumnType.eNumber, colAlign: 'center' }, diff --git a/server/graphql/schema/systemobject/resolvers/queries/getVersionsForAsset.ts b/server/graphql/schema/systemobject/resolvers/queries/getVersionsForAsset.ts index 158d45dc7..101b2bc51 100644 --- a/server/graphql/schema/systemobject/resolvers/queries/getVersionsForAsset.ts +++ b/server/graphql/schema/systemobject/resolvers/queries/getVersionsForAsset.ts @@ -39,6 +39,7 @@ export default async function getVersionsForAsset(_: Parent, args: QueryGetVersi creator: user ? user.Name : '', dateCreated: assetVersion.DateCreated, size: assetVersion.StorageSize, + hash: assetVersion.StorageHash, ingested: assetVersion.Ingested ?? false, Comment: assetVersion.Comment, CommentLink: assetVersion.Comment && assetVersion.Comment.length <= 300 diff --git a/server/types/graphql.ts b/server/types/graphql.ts index 19203a2da..52c1f03a8 100644 --- a/server/types/graphql.ts +++ b/server/types/graphql.ts @@ -1589,6 +1589,7 @@ export type AssetVersionDetailFieldsInput = { Ingested?: Maybe; Version?: Maybe; StorageSize?: Maybe; + StorageHash?: Maybe; }; export type ActorDetailFieldsInput = { @@ -1871,6 +1872,7 @@ export type AssetVersionDetailFields = { AssetVersion?: Maybe; idAsset?: Maybe; idAssetVersion?: Maybe; + StorageHash?: Maybe; }; export type ActorDetailFields = { @@ -1986,6 +1988,7 @@ export type DetailVersion = { creator: Scalars['String']; dateCreated: Scalars['DateTime']; size: Scalars['BigInt']; + hash: Scalars['String']; ingested: Scalars['Boolean']; Comment?: Maybe; CommentLink?: Maybe; From 9aa83b3b4253fa40796141c36352f8e592ef5cc9 Mon Sep 17 00:00:00 2001 From: Hsin Tung Date: Wed, 9 Mar 2022 11:36:29 -0800 Subject: [PATCH 03/31] address lint --- .../Repository/components/DetailsView/DetailsTab/AssetGrid.tsx | 2 +- .../components/DetailsView/DetailsTab/AssetVersionDetails.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/pages/Repository/components/DetailsView/DetailsTab/AssetGrid.tsx b/client/src/pages/Repository/components/DetailsView/DetailsTab/AssetGrid.tsx index 039dc52cd..31a223f78 100644 --- a/client/src/pages/Repository/components/DetailsView/DetailsTab/AssetGrid.tsx +++ b/client/src/pages/Repository/components/DetailsView/DetailsTab/AssetGrid.tsx @@ -316,7 +316,7 @@ function AssetGrid(props: AssetGridProps): React.ReactElement { ); } - } + }; break; } result.push(gridColumnObject); diff --git a/client/src/pages/Repository/components/DetailsView/DetailsTab/AssetVersionDetails.tsx b/client/src/pages/Repository/components/DetailsView/DetailsTab/AssetVersionDetails.tsx index 221e64e2c..012b16c0b 100644 --- a/client/src/pages/Repository/components/DetailsView/DetailsTab/AssetVersionDetails.tsx +++ b/client/src/pages/Repository/components/DetailsView/DetailsTab/AssetVersionDetails.tsx @@ -100,7 +100,7 @@ function AssetVersionDetails(props: DetailComponentProps): React.ReactElement { name='FilePath' onChange={onSetField} debounceTimeout={400} - style={{ width: '300px', height: 18, ...updatedFieldStyling(isFieldUpdated(AssetVersionDetails, assetVersionData, 'FilePath'))}} + style={{ width: '300px', height: 18, ...updatedFieldStyling(isFieldUpdated(AssetVersionDetails, assetVersionData, 'FilePath')) }} /> From 8ba11c2f0a290eb42b671a1960cbb24d2fc785bf Mon Sep 17 00:00:00 2001 From: Jon Tyson <6943745+jahjedtieson@users.noreply.github.com> Date: Tue, 22 Mar 2022 14:52:09 -0700 Subject: [PATCH 04/31] GraphQL: * Modified updateObjectDetails to take an optional Subtitle as input, needed by Items, Models, and Scenes * Modified getSystemObjectDetails to return an optional subTitle for Items, Models, and Scenes --- client/src/types/graphql.tsx | 2 + server/graphql/schema.graphql | 2 + .../schema/systemobject/mutations.graphql | 1 + .../schema/systemobject/queries.graphql | 1 + .../mutations/updateObjectDetails.ts | 19 +++++++-- .../queries/getSystemObjectDetails.ts | 42 ++++++++++++++++++- server/types/graphql.ts | 2 + 7 files changed, 64 insertions(+), 5 deletions(-) diff --git a/client/src/types/graphql.tsx b/client/src/types/graphql.tsx index 4eead6a1d..08779e3ac 100644 --- a/client/src/types/graphql.tsx +++ b/client/src/types/graphql.tsx @@ -1616,6 +1616,7 @@ export type MetadataInput = { export type UpdateObjectDetailsDataInput = { Name?: Maybe; + Subtitle?: Maybe; Retired?: Maybe; License?: Maybe; Unit?: Maybe; @@ -1925,6 +1926,7 @@ export type GetSystemObjectDetailsResult = { idSystemObject: Scalars['Int']; idObject: Scalars['Int']; name: Scalars['String']; + subTitle?: Maybe; retired: Scalars['Boolean']; objectType: Scalars['Int']; allowed: Scalars['Boolean']; diff --git a/server/graphql/schema.graphql b/server/graphql/schema.graphql index 23a64038a..97c347485 100644 --- a/server/graphql/schema.graphql +++ b/server/graphql/schema.graphql @@ -1166,6 +1166,7 @@ input MetadataInput { input UpdateObjectDetailsDataInput { Name: String + Subtitle: String Retired: Boolean License: Int Unit: UnitDetailFieldsInput @@ -1452,6 +1453,7 @@ type GetSystemObjectDetailsResult { idSystemObject: Int! idObject: Int! name: String! + subTitle: String retired: Boolean! objectType: Int! allowed: Boolean! diff --git a/server/graphql/schema/systemobject/mutations.graphql b/server/graphql/schema/systemobject/mutations.graphql index 2fe152506..8d43c60f9 100644 --- a/server/graphql/schema/systemobject/mutations.graphql +++ b/server/graphql/schema/systemobject/mutations.graphql @@ -132,6 +132,7 @@ input MetadataInput { input UpdateObjectDetailsDataInput { Name: String + Subtitle: String Retired: Boolean License: Int Unit: UnitDetailFieldsInput diff --git a/server/graphql/schema/systemobject/queries.graphql b/server/graphql/schema/systemobject/queries.graphql index d5e508e6f..3a9d3fe2d 100644 --- a/server/graphql/schema/systemobject/queries.graphql +++ b/server/graphql/schema/systemobject/queries.graphql @@ -162,6 +162,7 @@ type GetSystemObjectDetailsResult { idSystemObject: Int! idObject: Int! name: String! + subTitle: String retired: Boolean! objectType: Int! allowed: Boolean! diff --git a/server/graphql/schema/systemobject/resolvers/mutations/updateObjectDetails.ts b/server/graphql/schema/systemobject/resolvers/mutations/updateObjectDetails.ts index 5e6fc8454..fd7eeab72 100644 --- a/server/graphql/schema/systemobject/resolvers/mutations/updateObjectDetails.ts +++ b/server/graphql/schema/systemobject/resolvers/mutations/updateObjectDetails.ts @@ -181,7 +181,9 @@ export default async function updateObjectDetails(_: Parent, args: MutationUpdat if (!Item) return sendResult(false, `Unable to fetch Media Group with id ${idObject}; update failed`); - Item.Name = data.Name; + Item.Name = computeNewName(Item.Name, Item.Title, data.Subtitle); // do this before updating .Title + Item.Title = data.Subtitle ?? null; + if (!isNull(EntireSubject) && !isUndefined(EntireSubject)) Item.EntireSubject = EntireSubject; @@ -313,7 +315,6 @@ export default async function updateObjectDetails(_: Parent, args: MutationUpdat return sendResult(false, `Unable to fetch ${SystemObjectTypeToName(objectType)} with id ${idObject}; update failed`); const { - Name, DateCreated, CreationMethod, Modality, @@ -322,7 +323,9 @@ export default async function updateObjectDetails(_: Parent, args: MutationUpdat ModelFileType } = data.Model; - if (Name) Model.Name = Name; + Model.Name = computeNewName(Model.Name, Model.Title, data.Subtitle); // do this before updating .Title + Model.Title = data.Subtitle ?? null; + if (CreationMethod) Model.idVCreationMethod = CreationMethod; if (Modality) Model.idVModality = Modality; if (Purpose) Model.idVPurpose = Purpose; @@ -341,7 +344,10 @@ export default async function updateObjectDetails(_: Parent, args: MutationUpdat return sendResult(false, `Unable to fetch ${SystemObjectTypeToName(objectType)} with id ${idObject}; update failed`); const oldPosedAndQCd: boolean = Scene.PosedAndQCd; - Scene.Name = data.Name; + + Scene.Name = computeNewName(Scene.Name, Scene.Title, data.Subtitle); // do this before updated .Title + Scene.Title = data.Subtitle ?? null; + if (data.Scene) { if (typeof data.Scene.PosedAndQCd === 'boolean') Scene.PosedAndQCd = data.Scene.PosedAndQCd; if (typeof data.Scene.ApprovedForPublication === 'boolean') Scene.ApprovedForPublication = data.Scene.ApprovedForPublication; @@ -512,3 +518,8 @@ export async function handleMetadata(idSystemObject: number, metadatas: Metadata } return { success: true }; } + +function computeNewName(oldName: string, oldTitle: string | null, newTitle: string | null | undefined): string { + return ((!oldTitle) ? oldName : oldName.replace(`: ${oldTitle}`, '')) // strip off old title + + (newTitle) ? `: ${newTitle}` : ''; +} \ No newline at end of file diff --git a/server/graphql/schema/systemobject/resolvers/queries/getSystemObjectDetails.ts b/server/graphql/schema/systemobject/resolvers/queries/getSystemObjectDetails.ts index b0b52e3c3..c26ced8bc 100644 --- a/server/graphql/schema/systemobject/resolvers/queries/getSystemObjectDetails.ts +++ b/server/graphql/schema/systemobject/resolvers/queries/getSystemObjectDetails.ts @@ -66,6 +66,7 @@ export default async function getSystemObjectDetails(_: Parent, args: QueryGetSy const { owner: assetOwner, asset } = await computeAssetAndOwner(oID); const name: string = await resolveNameForObject(idSystemObject); + const subTitle: string | null = await resolveSubtitleForObject(idSystemObject); // LOG.info('getSystemObjectDetails 3', LOG.LS.eGQL); const metadata: DBAPI.Metadata[] | null = await DBAPI.Metadata.fetchFromSystemObject(idSystemObject); @@ -79,6 +80,7 @@ export default async function getSystemObjectDetails(_: Parent, args: QueryGetSy idSystemObject, idObject: oID.idObject, name, + subTitle, retired: systemObject.Retired, objectType: oID.eObjectType, allowed: true, // TODO: True until Access control is implemented (Post MVP) @@ -213,6 +215,44 @@ async function resolveNameForObject(idSystemObject: number): Promise { return name ?? UNKNOWN_NAME; } +async function resolveSubtitleForObject(idSystemObject: number): Promise { + const oID: DBAPI.ObjectIDAndType | undefined = await CACHE.SystemObjectCache.getObjectFromSystem(idSystemObject); + if (!oID) { + LOG.error(`getSystemObjectDetails resolveSubtitleForObject failed to compute object ID and type for ${idSystemObject}`, LOG.LS.eGQL); + return null; + } + + switch (oID.eObjectType) { + case COMMON.eSystemObjectType.eItem: { + const item: DBAPI.Item | null = await DBAPI.Item.fetch(oID.idObject); + if (!item) { + LOG.error(`getSystemObjectDetails resolveSubtitleForObject unable to load item with id ${oID.idObject}`, LOG.LS.eGQL); + return null; + } + return item.Title; + } + + case COMMON.eSystemObjectType.eModel: { + const model: DBAPI.Model | null = await DBAPI.Model.fetch(oID.idObject); + if (!model) { + LOG.error(`getSystemObjectDetails resolveSubtitleForObject unable to load model with id ${oID.idObject}`, LOG.LS.eGQL); + return null; + } + return model.Title; + } + + case COMMON.eSystemObjectType.eScene: { + const scene: DBAPI.Scene | null = await DBAPI.Scene.fetch(oID.idObject); + if (!scene) { + LOG.error(`getSystemObjectDetails resolveSubtitleForObject unable to load scene with id ${oID.idObject}`, LOG.LS.eGQL); + return null; + } + return scene.Title; + } + } + return null; +} + async function computeAssetAndOwner(oID: DBAPI.ObjectIDAndType): Promise<{ owner: RepositoryPath | undefined, asset: RepositoryPath | undefined }> { let idAsset: number | undefined = undefined; let owner: RepositoryPath | undefined = undefined; @@ -247,7 +287,7 @@ async function computeAssetAndOwner(oID: DBAPI.ObjectIDAndType): Promise<{ owner return { owner, asset }; } - const assetName: string | undefined = await resolveNameForObject(SOAsset.idSystemObject); + const assetName: string = await resolveNameForObject(SOAsset.idSystemObject); asset = { idSystemObject: SOAsset.idSystemObject, name: assetName ?? '', objectType: COMMON.eSystemObjectType.eAsset }; if (!assetDB.idSystemObject) diff --git a/server/types/graphql.ts b/server/types/graphql.ts index 52c1f03a8..b74b6905a 100644 --- a/server/types/graphql.ts +++ b/server/types/graphql.ts @@ -1613,6 +1613,7 @@ export type MetadataInput = { export type UpdateObjectDetailsDataInput = { Name?: Maybe; + Subtitle?: Maybe; Retired?: Maybe; License?: Maybe; Unit?: Maybe; @@ -1922,6 +1923,7 @@ export type GetSystemObjectDetailsResult = { idSystemObject: Scalars['Int']; idObject: Scalars['Int']; name: Scalars['String']; + subTitle?: Maybe; retired: Scalars['Boolean']; objectType: Scalars['Int']; allowed: Scalars['Boolean']; From bcc408a73ff2ed5e530211722ca059fede522ccd Mon Sep 17 00:00:00 2001 From: Jon Tyson <6943745+jahjedtieson@users.noreply.github.com> Date: Tue, 22 Mar 2022 17:48:01 -0700 Subject: [PATCH 05/31] GraphQL: * Validate ingestData input and return on error before creating workflow and related objects --- .../schema/ingestion/resolvers/mutations/ingestData.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/graphql/schema/ingestion/resolvers/mutations/ingestData.ts b/server/graphql/schema/ingestion/resolvers/mutations/ingestData.ts index cd638f3fd..66b71b368 100644 --- a/server/graphql/schema/ingestion/resolvers/mutations/ingestData.ts +++ b/server/graphql/schema/ingestion/resolvers/mutations/ingestData.ts @@ -104,9 +104,10 @@ class IngestDataWorker extends ResolverBase { LOG.info(`ingestData: input=${JSON.stringify(this.input, H.Helpers.saferStringify)}`, LOG.LS.eGQL); const results: H.IOResults = await this.validateInput(); - this.workflowHelper = await this.createWorkflow(); // do this *after* this.validateInput, and *before* returning from validation failure - if (!results.success) return { success: results.success, message: results.error }; + if (!results.success) + return { success: results.success, message: results.error }; + this.workflowHelper = await this.createWorkflow(); // do this *after* this.validateInput, and *after* returning from validation failure, to avoid creating ingestion workflows that failed due to validation issues const subjectsDB: DBAPI.Subject[] = []; let itemDB: DBAPI.Item | null = null; if (this.ingestNew) { From 310f0576bbbb25bd4dc13c037c3ff6c39b2f9dcf Mon Sep 17 00:00:00 2001 From: Jon Tyson <6943745+jahjedtieson@users.noreply.github.com> Date: Wed, 23 Mar 2022 16:39:19 -0700 Subject: [PATCH 06/31] DBAPI: * Renamed ModelSceneXref.updateTransformIfNeeded to updateIfNeeded, and update all non-DB ID fields, both transform-related and others GraphQL: * If updateObjectDetails input contains a name and no Subtitle, use the name for Item, Model, and Scene updates. * Add Bounding Box column output to scene asset list * During ingestData, use updated ModelSceneXref.updateIfNeeded to update all non-DB ID fields (transform and others) Storage: * During scene ingestion (such as via WebDAV updates by Voyager Story), use updated ModelSceneXref.updateIfNeeded to update all non-DB ID fields (transform and others) --- server/db/api/ModelSceneXref.ts | 47 +++++--- .../resolvers/mutations/ingestData.ts | 12 ++- .../mutations/updateObjectDetails.ts | 13 ++- .../resolvers/queries/AssetGridDetailScene.ts | 5 +- .../queries/getAssetDetailsForSystemObject.ts | 13 ++- .../storage/interface/AssetStorageAdapter.ts | 7 +- server/tests/db/dbcreation.test.ts | 101 ++++++++++++++++-- 7 files changed, 155 insertions(+), 43 deletions(-) diff --git a/server/db/api/ModelSceneXref.ts b/server/db/api/ModelSceneXref.ts index b47fb3b2d..e12549e26 100644 --- a/server/db/api/ModelSceneXref.ts +++ b/server/db/api/ModelSceneXref.ts @@ -49,22 +49,39 @@ export class ModelSceneXref extends DBC.DBObject implements && this.S2 === MSX.S2; } /** return true if transform is updated */ - public updateTransformIfNeeded(MSX: ModelSceneXref): boolean { - let retValue: boolean = false; + public updateIfNeeded(MSX: ModelSceneXref): { transformUpdated: boolean, updated: boolean } { + let updated: boolean = false; + let transformUpdated: boolean = false; const logContext: string = `${H.Helpers.JSONStringify(this)} vs incoming ${H.Helpers.JSONStringify(MSX)}`; - if (H.Helpers.safeRound(this.TS0) !== H.Helpers.safeRound(MSX.TS0)) { this.TS0 = MSX.TS0; retValue = true; } - if (H.Helpers.safeRound(this.TS1) !== H.Helpers.safeRound(MSX.TS1)) { this.TS1 = MSX.TS1; retValue = true; } - if (H.Helpers.safeRound(this.TS2) !== H.Helpers.safeRound(MSX.TS2)) { this.TS2 = MSX.TS2; retValue = true; } - if (H.Helpers.safeRound(this.R0) !== H.Helpers.safeRound(MSX.R0)) { this.R0 = MSX.R0; retValue = true; } - if (H.Helpers.safeRound(this.R1) !== H.Helpers.safeRound(MSX.R1)) { this.R1 = MSX.R1; retValue = true; } - if (H.Helpers.safeRound(this.R2) !== H.Helpers.safeRound(MSX.R2)) { this.R2 = MSX.R2; retValue = true; } - if (H.Helpers.safeRound(this.R3) !== H.Helpers.safeRound(MSX.R3)) { this.R3 = MSX.R3; retValue = true; } - if (H.Helpers.safeRound(this.S0) !== H.Helpers.safeRound(MSX.S0)) { this.S0 = MSX.S0; retValue = true; } - if (H.Helpers.safeRound(this.S1) !== H.Helpers.safeRound(MSX.S1)) { this.S1 = MSX.S1; retValue = true; } - if (H.Helpers.safeRound(this.S2) !== H.Helpers.safeRound(MSX.S2)) { this.S2 = MSX.S2; retValue = true; } - if (retValue) - LOG.info(`ModelSceneXref.updateTransformIfNeeded UPDATED: ${logContext}`, LOG.LS.eDB); - return retValue; + + if (this.Name !== MSX.Name) { this.Name = MSX.Name; updated = true; } + if (this.Usage !== MSX.Usage) { this.Usage = MSX.Usage; updated = true; } + if (this.Quality !== MSX.Quality) { this.Quality = MSX.Quality; updated = true; } + if (this.FileSize !== MSX.FileSize) { this.FileSize = MSX.FileSize; updated = true; } + if (this.UVResolution !== MSX.UVResolution) { this.UVResolution = MSX.UVResolution; updated = true; } + if (H.Helpers.safeRound(this.BoundingBoxP1X) !== H.Helpers.safeRound(MSX.BoundingBoxP1X)) { this.BoundingBoxP1X = MSX.BoundingBoxP1X; updated = true; } + if (H.Helpers.safeRound(this.BoundingBoxP1Y) !== H.Helpers.safeRound(MSX.BoundingBoxP1Y)) { this.BoundingBoxP1Y = MSX.BoundingBoxP1Y; updated = true; } + if (H.Helpers.safeRound(this.BoundingBoxP1Z) !== H.Helpers.safeRound(MSX.BoundingBoxP1Z)) { this.BoundingBoxP1Z = MSX.BoundingBoxP1Z; updated = true; } + if (H.Helpers.safeRound(this.BoundingBoxP2X) !== H.Helpers.safeRound(MSX.BoundingBoxP2X)) { this.BoundingBoxP2X = MSX.BoundingBoxP2X; updated = true; } + if (H.Helpers.safeRound(this.BoundingBoxP2Y) !== H.Helpers.safeRound(MSX.BoundingBoxP2Y)) { this.BoundingBoxP2Y = MSX.BoundingBoxP2Y; updated = true; } + if (H.Helpers.safeRound(this.BoundingBoxP2Z) !== H.Helpers.safeRound(MSX.BoundingBoxP2Z)) { this.BoundingBoxP2Z = MSX.BoundingBoxP2Z; updated = true; } + + if (H.Helpers.safeRound(this.TS0) !== H.Helpers.safeRound(MSX.TS0)) { this.TS0 = MSX.TS0; transformUpdated = true; } + if (H.Helpers.safeRound(this.TS1) !== H.Helpers.safeRound(MSX.TS1)) { this.TS1 = MSX.TS1; transformUpdated = true; } + if (H.Helpers.safeRound(this.TS2) !== H.Helpers.safeRound(MSX.TS2)) { this.TS2 = MSX.TS2; transformUpdated = true; } + if (H.Helpers.safeRound(this.R0) !== H.Helpers.safeRound(MSX.R0)) { this.R0 = MSX.R0; transformUpdated = true; } + if (H.Helpers.safeRound(this.R1) !== H.Helpers.safeRound(MSX.R1)) { this.R1 = MSX.R1; transformUpdated = true; } + if (H.Helpers.safeRound(this.R2) !== H.Helpers.safeRound(MSX.R2)) { this.R2 = MSX.R2; transformUpdated = true; } + if (H.Helpers.safeRound(this.R3) !== H.Helpers.safeRound(MSX.R3)) { this.R3 = MSX.R3; transformUpdated = true; } + if (H.Helpers.safeRound(this.S0) !== H.Helpers.safeRound(MSX.S0)) { this.S0 = MSX.S0; transformUpdated = true; } + if (H.Helpers.safeRound(this.S1) !== H.Helpers.safeRound(MSX.S1)) { this.S1 = MSX.S1; transformUpdated = true; } + if (H.Helpers.safeRound(this.S2) !== H.Helpers.safeRound(MSX.S2)) { this.S2 = MSX.S2; transformUpdated = true; } + + if (transformUpdated) + updated = true; + if (updated) + LOG.info(`ModelSceneXref.updateTransformIfNeeded ${transformUpdated ? 'TRANSFORM UPDATED' : 'UPDATED'}: ${logContext}`, LOG.LS.eDB); + return { transformUpdated, updated }; } public computeModelAutomationTag(): string { diff --git a/server/graphql/schema/ingestion/resolvers/mutations/ingestData.ts b/server/graphql/schema/ingestion/resolvers/mutations/ingestData.ts index 66b71b368..82f47d73d 100644 --- a/server/graphql/schema/ingestion/resolvers/mutations/ingestData.ts +++ b/server/graphql/schema/ingestion/resolvers/mutations/ingestData.ts @@ -948,10 +948,11 @@ class IngestDataWorker extends ResolverBase { const MSXExisting: DBAPI.ModelSceneXref[] | null = await DBAPI.ModelSceneXref.fetchFromModelAndScene(MSX.idModel, sceneDB.idScene); let MSXUpdate: DBAPI.ModelSceneXref | null = (MSXExisting && MSXExisting.length > 0) ? MSXExisting[0] : null; if (MSXUpdate) { - if (MSXUpdate.updateTransformIfNeeded(MSX)) { + const { transformUpdated: transformUpdatedLocal, updated } = MSXUpdate.updateIfNeeded(MSX); + if (updated) success = await MSXUpdate.update(); + if (transformUpdatedLocal) transformUpdated = true; - } } else { MSX.idScene = sceneDB.idScene; success = await MSX.create() && success; @@ -1652,13 +1653,16 @@ class IngestDataWorker extends ResolverBase { await DBAPI.ModelSceneXref.fetchFromSceneNameUsageQualityUVResolution(scene.idScene, MSX.Name, MSX.Usage, MSX.Quality, MSX.UVResolution); const MSXSource: DBAPI.ModelSceneXref | null = (MSXSources && MSXSources.length > 0) ? MSXSources[0] : null; if (MSXSource) { - if (MSXSource.updateTransformIfNeeded(MSX)) { + const { transformUpdated: transformUpdatedLocal, updated } = MSXSource.updateIfNeeded(MSX); + + if (updated) { if (!await MSXSource.update()) { LOG.error(`ingestData handleComplexIngestionScene unable to update ModelSceneXref ${JSON.stringify(MSXSource, H.Helpers.saferStringify)}`, LOG.LS.eGQL); success = false; } - transformUpdated = true; } + if (transformUpdatedLocal) + transformUpdated = true; model = await DBAPI.Model.fetch(MSXSource.idModel); if (!model) { diff --git a/server/graphql/schema/systemobject/resolvers/mutations/updateObjectDetails.ts b/server/graphql/schema/systemobject/resolvers/mutations/updateObjectDetails.ts index fd7eeab72..f1e8dc875 100644 --- a/server/graphql/schema/systemobject/resolvers/mutations/updateObjectDetails.ts +++ b/server/graphql/schema/systemobject/resolvers/mutations/updateObjectDetails.ts @@ -181,7 +181,7 @@ export default async function updateObjectDetails(_: Parent, args: MutationUpdat if (!Item) return sendResult(false, `Unable to fetch Media Group with id ${idObject}; update failed`); - Item.Name = computeNewName(Item.Name, Item.Title, data.Subtitle); // do this before updating .Title + Item.Name = (data.Name && !data.Subtitle) ? data.Name : computeNewName(Item.Name, Item.Title, data.Subtitle); // do this before updating .Title Item.Title = data.Subtitle ?? null; if (!isNull(EntireSubject) && !isUndefined(EntireSubject)) @@ -323,7 +323,7 @@ export default async function updateObjectDetails(_: Parent, args: MutationUpdat ModelFileType } = data.Model; - Model.Name = computeNewName(Model.Name, Model.Title, data.Subtitle); // do this before updating .Title + Model.Name = (data.Name && !data.Subtitle) ? data.Name : computeNewName(Model.Name, Model.Title, data.Subtitle); // do this before updating .Title Model.Title = data.Subtitle ?? null; if (CreationMethod) Model.idVCreationMethod = CreationMethod; @@ -345,7 +345,7 @@ export default async function updateObjectDetails(_: Parent, args: MutationUpdat const oldPosedAndQCd: boolean = Scene.PosedAndQCd; - Scene.Name = computeNewName(Scene.Name, Scene.Title, data.Subtitle); // do this before updated .Title + Scene.Name = (data.Name && !data.Subtitle) ? data.Name : computeNewName(Scene.Name, Scene.Title, data.Subtitle); // do this before updated .Title Scene.Title = data.Subtitle ?? null; if (data.Scene) { @@ -520,6 +520,9 @@ export async function handleMetadata(idSystemObject: number, metadatas: Metadata } function computeNewName(oldName: string, oldTitle: string | null, newTitle: string | null | undefined): string { - return ((!oldTitle) ? oldName : oldName.replace(`: ${oldTitle}`, '')) // strip off old title - + (newTitle) ? `: ${newTitle}` : ''; + const oldBaseName: string = (!oldTitle) ? oldName : oldName.replace(`: ${oldTitle}`, ''); // strip off old title + const newName: string = oldBaseName + ((newTitle) ? `: ${newTitle}` : ''); + // LOG.info(`updateObjectDetails computeNewName(${oldName}, ${oldTitle}, ${newTitle}) = ${newName} (oldBaseName = ${oldBaseName})`, LOG.LS.eGQL); + + return newName; } \ No newline at end of file diff --git a/server/graphql/schema/systemobject/resolvers/queries/AssetGridDetailScene.ts b/server/graphql/schema/systemobject/resolvers/queries/AssetGridDetailScene.ts index 6dd73f809..404ce8cc2 100644 --- a/server/graphql/schema/systemobject/resolvers/queries/AssetGridDetailScene.ts +++ b/server/graphql/schema/systemobject/resolvers/queries/AssetGridDetailScene.ts @@ -20,6 +20,7 @@ export class AssetGridDetailScene extends AssetGridDetailBase { // usage: string | null; quality: string | null; uvResolution: number | null; + boundingBox: string | null; isAttachment: boolean | null; type: string | null; @@ -44,6 +45,7 @@ export class AssetGridDetailScene extends AssetGridDetailBase { // this.usage = H.Helpers.safeString(metadataMap.get('usage')); this.quality = H.Helpers.safeString(metadataMap.get('quality')); this.uvResolution = H.Helpers.safeNumber(metadataMap.get('uvresolution')); + this.boundingBox = H.Helpers.safeString(metadataMap.get('boundingbox')); this.isAttachment = H.Helpers.safeBoolean(metadataMap.get('isattachment')); this.type = H.Helpers.safeString(metadataMap.get('type')); @@ -70,6 +72,7 @@ export class AssetGridDetailScene extends AssetGridDetailBase { // { colName: 'usage', colLabel: 'Usage', colDisplay: true, colType: COMMON.eAssetGridColumnType.eString, colAlign: 'left' }, { colName: 'quality', colLabel: 'Quality', colDisplay: true, colType: COMMON.eAssetGridColumnType.eString, colAlign: 'center' }, { colName: 'uvResolution', colLabel: 'UV', colDisplay: true, colType: COMMON.eAssetGridColumnType.eNumber, colAlign: 'center' }, + { colName: 'boundingBox', colLabel: 'Bounding Box', colDisplay: true, colType: COMMON.eAssetGridColumnType.eString, colAlign: 'center' }, { colName: 'isAttachment', colLabel: 'Att?', colDisplay: true, colType: COMMON.eAssetGridColumnType.eBoolean, colAlign: 'center' }, { colName: 'type', colLabel: 'Type', colDisplay: true, colType: COMMON.eAssetGridColumnType.eString, colAlign: 'left' }, @@ -84,7 +87,7 @@ export class AssetGridDetailScene extends AssetGridDetailBase { } static getMetadataColumnNames(): string[] { - return [/* 'usage', */ 'quality', 'uvresolution', 'isattachment', 'type', 'category', 'units', 'modeltype', 'filetype', 'gltfstandardized', 'dracocompressed', 'title']; + return [/* 'usage', */ 'quality', 'uvresolution', 'boundingbox', 'isattachment', 'type', 'category', 'units', 'modeltype', 'filetype', 'gltfstandardized', 'dracocompressed', 'title']; } } diff --git a/server/graphql/schema/systemobject/resolvers/queries/getAssetDetailsForSystemObject.ts b/server/graphql/schema/systemobject/resolvers/queries/getAssetDetailsForSystemObject.ts index 9b2991c60..fcb71ca82 100644 --- a/server/graphql/schema/systemobject/resolvers/queries/getAssetDetailsForSystemObject.ts +++ b/server/graphql/schema/systemobject/resolvers/queries/getAssetDetailsForSystemObject.ts @@ -3,6 +3,7 @@ import * as DBAPI from '../../../../../db'; import * as CACHE from '../../../../../cache'; import * as NAV from '../../../../../navigation/interface'; import * as LOG from '../../../../../utils/logger'; +// import * as H from '../../../../../utils/helpers'; import { ColumnDefinition, GetAssetDetailsForSystemObjectResult, QueryGetAssetDetailsForSystemObjectArgs } from '../../../../../types/graphql'; import { Parent } from '../../../../../types/resolvers'; import { VocabularyCache } from '../../../../../cache'; @@ -167,6 +168,7 @@ async function extractMetadata(idSystemObject: number, metadataColumns: string[] async function extractSceneAttachmentMetadata(idScene: number, metadataMetaMap: Map>): Promise { const MSXs: DBAPI.ModelSceneXref[] | null = await DBAPI.ModelSceneXref.fetchFromScene(idScene); + // LOG.info(`getAssetDetailsForSystemObject MSXs ${H.Helpers.JSONStringify(MSXs)}`, LOG.LS.eGQL); if (!MSXs) { LOG.error(`getAssetDetailsForSystemObject extractSceneAttachmentMetadata failed to fetch ModelSceneXref for scene ${idScene}`, LOG.LS.eGQL); return false; @@ -202,13 +204,14 @@ async function extractSceneAttachmentMetadata(idScene: number, metadataMetaMap: metadataMap.set('quality', MSX.Quality); if (MSX.UVResolution) metadataMap.set('uvresolution', MSX.UVResolution.toString()); + if (MSX.BoundingBoxP1X && MSX.BoundingBoxP1Y && MSX.BoundingBoxP1Z && MSX.BoundingBoxP2X && MSX.BoundingBoxP2Y && MSX.BoundingBoxP2Z) + metadataMap.set('boundingbox', `(${round(MSX.BoundingBoxP1X)}, ${round(MSX.BoundingBoxP1Y)}, ${round(MSX.BoundingBoxP1Z)}) - (${round(MSX.BoundingBoxP2X)}, ${round(MSX.BoundingBoxP2Y)}, ${round(MSX.BoundingBoxP2Z)})`); + // LOG.info(`getAssetDetailsForSystemObject metadataMap[${SOIAV.idSystemObject}]=${H.Helpers.JSONStringify(metadataMap)}`, LOG.LS.eGQL); } - // if (MSX.BoundingBoxP1X && MSX.BoundingBoxP1Y && MSX.BoundingBoxP1Z && MSX.BoundingBoxP2X && MSX.BoundingBoxP2Y && MSX.BoundingBoxP2Z) - // metadataMap.set('boundingbox', `(${round(MSX.BoundingBoxP1X)}, ${round(MSX.BoundingBoxP1Y)}, ${round(MSX.BoundingBoxP1Z)}) - (${round(MSX.BoundingBoxP2X)}, ${round(MSX.BoundingBoxP2Y)}, ${round(MSX.BoundingBoxP2Z)})`); } return true; } -// function round(num: number): string { -// return (Math.ceil(num * 100) / 100).toString(); -// } +function round(num: number): string { + return (Math.ceil(num * 100) / 100).toString(); +} diff --git a/server/storage/interface/AssetStorageAdapter.ts b/server/storage/interface/AssetStorageAdapter.ts index 9cc7e8e15..fee7ef0c9 100644 --- a/server/storage/interface/AssetStorageAdapter.ts +++ b/server/storage/interface/AssetStorageAdapter.ts @@ -682,13 +682,16 @@ export class AssetStorageAdapter { const MSXSource: DBAPI.ModelSceneXref | null = (MSXSources && MSXSources.length > 0) ? MSXSources[0] : null; if (MSXSource) { LOG.info(`AssetStorageAdapter.detectAndHandleSceneIngest found existing ModelSceneXref=${JSON.stringify(MSXSource, H.Helpers.saferStringify)} from referenced model ${JSON.stringify(MSX, H.Helpers.saferStringify)}`, LOG.LS.eSTR); - if (MSXSource.updateTransformIfNeeded(MSX)) { + const { transformUpdated: transformUpdatedLocal, updated } = MSXSource.updateIfNeeded(MSX); + + if (updated) { if (!await MSXSource.update()) { LOG.error(`AssetStorageAdapter.detectAndHandleSceneIngest unable to update ModelSceneXref ${JSON.stringify(MSXSource, H.Helpers.saferStringify)}`, LOG.LS.eSTR); success = false; } - transformUpdated = true; } + if (transformUpdatedLocal) + transformUpdated = true; } else { // Could not find the ModelSceneXref transformUpdated = true; diff --git a/server/tests/db/dbcreation.test.ts b/server/tests/db/dbcreation.test.ts index 629e7594b..e1def67f8 100644 --- a/server/tests/db/dbcreation.test.ts +++ b/server/tests/db/dbcreation.test.ts @@ -5355,52 +5355,131 @@ describe('DB Fetch Special Test Suite', () => { test('DB Update: ModelSceneXref.isTransformMatching', async () => { let modelSceneXrefClone: DBAPI.ModelSceneXref | null = null; + let transformUpdated: boolean = false; + let updated: boolean = false; if (modelSceneXref) { modelSceneXrefClone = new DBAPI.ModelSceneXref(modelSceneXref); expect(modelSceneXrefClone.isTransformMatching(modelSceneXref)).toBeTruthy(); - expect(modelSceneXrefClone.updateTransformIfNeeded(modelSceneXref)).toBeFalsy(); + ({ transformUpdated, updated } = modelSceneXrefClone.updateIfNeeded(modelSceneXref)); + expect(transformUpdated).toBeFalsy(); + expect(updated).toBeFalsy(); modelSceneXrefClone.S2 = (modelSceneXref.S2 ?? 0) + 1; expect(modelSceneXrefClone.isTransformMatching(modelSceneXref)).toBeFalsy(); - expect(modelSceneXrefClone.updateTransformIfNeeded(modelSceneXref)).toBeTruthy(); + ({ transformUpdated, updated } = modelSceneXrefClone.updateIfNeeded(modelSceneXref)); + expect(transformUpdated).toBeTruthy(); + expect(updated).toBeTruthy(); modelSceneXrefClone.S1 = (modelSceneXref.S1 ?? 0) + 1; expect(modelSceneXrefClone.isTransformMatching(modelSceneXref)).toBeFalsy(); - expect(modelSceneXrefClone.updateTransformIfNeeded(modelSceneXref)).toBeTruthy(); + ({ transformUpdated, updated } = modelSceneXrefClone.updateIfNeeded(modelSceneXref)); + expect(transformUpdated).toBeTruthy(); + expect(updated).toBeTruthy(); modelSceneXrefClone.S0 = (modelSceneXref.S0 ?? 0) + 1; expect(modelSceneXrefClone.isTransformMatching(modelSceneXref)).toBeFalsy(); - expect(modelSceneXrefClone.updateTransformIfNeeded(modelSceneXref)).toBeTruthy(); + ({ transformUpdated, updated } = modelSceneXrefClone.updateIfNeeded(modelSceneXref)); + expect(transformUpdated).toBeTruthy(); + expect(updated).toBeTruthy(); modelSceneXrefClone.R3 = (modelSceneXref.R3 ?? 0) + 1; expect(modelSceneXrefClone.isTransformMatching(modelSceneXref)).toBeFalsy(); - expect(modelSceneXrefClone.updateTransformIfNeeded(modelSceneXref)).toBeTruthy(); + ({ transformUpdated, updated } = modelSceneXrefClone.updateIfNeeded(modelSceneXref)); + expect(transformUpdated).toBeTruthy(); + expect(updated).toBeTruthy(); modelSceneXrefClone.R2 = (modelSceneXref.R2 ?? 0) + 1; expect(modelSceneXrefClone.isTransformMatching(modelSceneXref)).toBeFalsy(); - expect(modelSceneXrefClone.updateTransformIfNeeded(modelSceneXref)).toBeTruthy(); + ({ transformUpdated, updated } = modelSceneXrefClone.updateIfNeeded(modelSceneXref)); + expect(transformUpdated).toBeTruthy(); + expect(updated).toBeTruthy(); modelSceneXrefClone.R1 = (modelSceneXref.R1 ?? 0) + 1; expect(modelSceneXrefClone.isTransformMatching(modelSceneXref)).toBeFalsy(); - expect(modelSceneXrefClone.updateTransformIfNeeded(modelSceneXref)).toBeTruthy(); + ({ transformUpdated, updated } = modelSceneXrefClone.updateIfNeeded(modelSceneXref)); + expect(transformUpdated).toBeTruthy(); + expect(updated).toBeTruthy(); modelSceneXrefClone.R0 = (modelSceneXref.R0 ?? 0) + 1; expect(modelSceneXrefClone.isTransformMatching(modelSceneXref)).toBeFalsy(); - expect(modelSceneXrefClone.updateTransformIfNeeded(modelSceneXref)).toBeTruthy(); + ({ transformUpdated, updated } = modelSceneXrefClone.updateIfNeeded(modelSceneXref)); + expect(transformUpdated).toBeTruthy(); + expect(updated).toBeTruthy(); modelSceneXrefClone.TS2 = (modelSceneXref.TS2 ?? 0) + 1; expect(modelSceneXrefClone.isTransformMatching(modelSceneXref)).toBeFalsy(); - expect(modelSceneXrefClone.updateTransformIfNeeded(modelSceneXref)).toBeTruthy(); + ({ transformUpdated, updated } = modelSceneXrefClone.updateIfNeeded(modelSceneXref)); + expect(transformUpdated).toBeTruthy(); + expect(updated).toBeTruthy(); modelSceneXrefClone.TS1 = (modelSceneXref.TS1 ?? 0) + 1; expect(modelSceneXrefClone.isTransformMatching(modelSceneXref)).toBeFalsy(); - expect(modelSceneXrefClone.updateTransformIfNeeded(modelSceneXref)).toBeTruthy(); + ({ transformUpdated, updated } = modelSceneXrefClone.updateIfNeeded(modelSceneXref)); + expect(transformUpdated).toBeTruthy(); + expect(updated).toBeTruthy(); modelSceneXrefClone.TS0 = (modelSceneXref.TS0 ?? 0) + 1; expect(modelSceneXrefClone.isTransformMatching(modelSceneXref)).toBeFalsy(); - expect(modelSceneXrefClone.updateTransformIfNeeded(modelSceneXref)).toBeTruthy(); + ({ transformUpdated, updated } = modelSceneXrefClone.updateIfNeeded(modelSceneXref)); + expect(transformUpdated).toBeTruthy(); + expect(updated).toBeTruthy(); // LOG.info(`clone = ${JSON.stringify(modelSceneXrefClone, H.Helpers.saferStringify)} vs ${JSON.stringify(modelSceneXref, H.Helpers.saferStringify)}}`, LOG.LS.eTEST); + modelSceneXrefClone.BoundingBoxP2Z = (modelSceneXref.BoundingBoxP2Z ?? 0) + 1; + ({ transformUpdated, updated } = modelSceneXrefClone.updateIfNeeded(modelSceneXref)); + expect(transformUpdated).toBeFalsy(); + expect(updated).toBeTruthy(); + + modelSceneXrefClone.BoundingBoxP2Y = (modelSceneXref.BoundingBoxP2Y ?? 0) + 1; + ({ transformUpdated, updated } = modelSceneXrefClone.updateIfNeeded(modelSceneXref)); + expect(transformUpdated).toBeFalsy(); + expect(updated).toBeTruthy(); + + modelSceneXrefClone.BoundingBoxP2X = (modelSceneXref.BoundingBoxP2X ?? 0) + 1; + ({ transformUpdated, updated } = modelSceneXrefClone.updateIfNeeded(modelSceneXref)); + expect(transformUpdated).toBeFalsy(); + expect(updated).toBeTruthy(); + + modelSceneXrefClone.BoundingBoxP1Z = (modelSceneXref.BoundingBoxP1Z ?? 0) + 1; + ({ transformUpdated, updated } = modelSceneXrefClone.updateIfNeeded(modelSceneXref)); + expect(transformUpdated).toBeFalsy(); + expect(updated).toBeTruthy(); + + modelSceneXrefClone.BoundingBoxP1Y = (modelSceneXref.BoundingBoxP1Y ?? 0) + 1; + ({ transformUpdated, updated } = modelSceneXrefClone.updateIfNeeded(modelSceneXref)); + expect(transformUpdated).toBeFalsy(); + expect(updated).toBeTruthy(); + + modelSceneXrefClone.BoundingBoxP1X = (modelSceneXref.BoundingBoxP1X ?? 0) + 1; + ({ transformUpdated, updated } = modelSceneXrefClone.updateIfNeeded(modelSceneXref)); + expect(transformUpdated).toBeFalsy(); + expect(updated).toBeTruthy(); + + modelSceneXrefClone.UVResolution = (modelSceneXref.UVResolution ?? 0) + 1; + ({ transformUpdated, updated } = modelSceneXrefClone.updateIfNeeded(modelSceneXref)); + expect(transformUpdated).toBeFalsy(); + expect(updated).toBeTruthy(); + + modelSceneXrefClone.FileSize = (modelSceneXref.FileSize ?? BigInt(0)) + BigInt(1); + ({ transformUpdated, updated } = modelSceneXrefClone.updateIfNeeded(modelSceneXref)); + expect(transformUpdated).toBeFalsy(); + expect(updated).toBeTruthy(); + + modelSceneXrefClone.Quality = (modelSceneXref.Quality ?? '') + 'abba'; + ({ transformUpdated, updated } = modelSceneXrefClone.updateIfNeeded(modelSceneXref)); + expect(transformUpdated).toBeFalsy(); + expect(updated).toBeTruthy(); + + modelSceneXrefClone.Usage = (modelSceneXref.Usage ?? '') + 'abba'; + ({ transformUpdated, updated } = modelSceneXrefClone.updateIfNeeded(modelSceneXref)); + expect(transformUpdated).toBeFalsy(); + expect(updated).toBeTruthy(); + + modelSceneXrefClone.Name = (modelSceneXref.Name ?? '') + 'abba'; + ({ transformUpdated, updated } = modelSceneXrefClone.updateIfNeeded(modelSceneXref)); + expect(transformUpdated).toBeFalsy(); + expect(updated).toBeTruthy(); + const modelAutomationTag: string = modelSceneXrefClone.computeModelAutomationTag(); expect(modelAutomationTag.includes(modelSceneXrefClone.Usage ?? '')).toBeTruthy(); expect(modelAutomationTag.includes(modelSceneXrefClone.Quality ?? '')).toBeTruthy(); From ca0f3a1b73ea745d5d53d2a4b03cc95b002e738e Mon Sep 17 00:00:00 2001 From: Jon Tyson <6943745+jahjedtieson@users.noreply.github.com> Date: Fri, 25 Mar 2022 01:35:13 -0700 Subject: [PATCH 07/31] Client: * Implemented NonModelAssets control for Scene Ingestion. This imitates ReferenceModels, and presents non-model assets in a grid, separate from the model assets of a scene. For now, there is no way to upload a missing non-model asset. We'll add this post-MVP. * Updated language and layout of ReferenceModels control * Updated actions in ReferenceModels control -- use (uploaded) if the model is present in the upload; use the ingest link if the model is not in the upload and not in the system (not sure if that ingest link actually works); use the update link if the model is not in the upload but is in the system * Addressed client-side error in SceneDataForm due to use of a title attribute for a child object of Tooltip GraphQL: * Added SvxNonModelAsset type * Added SvxNonModelAssets to SceneConstellation * Added SvxNonModelAssets to getSceneForAssetVersion output DBAPI: * Updated SceneConstellation to compute non-model assets either by examining the svx.json file (when loading the constellation from a file) or by examining the scene's asset versions (when loading from the DB); guess about non-model asset 'type' by examining the asset versions' extension * When computing non-model assets from a file, look for them in the supplied file (i.e. if it's a zip); if found, record the asset version ID as -1 System: * Updated svxReader to parse non-model assets from .svx.json files, looking at both the meta -> images collection and the meta -> articles collection. * Avoid mis-identifying images as models * Handle multi-language articles --- .../Metadata/Scene/NonModelAssets.tsx | 163 ++++++++++++++++++ .../Metadata/Scene/ReferenceModels.tsx | 39 +++-- .../Metadata/Scene/SceneDataForm.tsx | 1 - .../components/Metadata/Scene/index.tsx | 25 ++- client/src/types/graphql.tsx | 22 ++- server/db/api/composite/SceneConstellation.ts | 108 +++++++++++- .../queries/scene/getSceneForAssetVersion.ts | 7 + server/graphql/schema.graphql | 9 + server/graphql/schema/scene/types.graphql | 9 + server/types/graphql.ts | 10 ++ server/utils/parser/svxReader.ts | 73 +++++++- 11 files changed, 430 insertions(+), 36 deletions(-) create mode 100644 client/src/pages/Ingestion/components/Metadata/Scene/NonModelAssets.tsx diff --git a/client/src/pages/Ingestion/components/Metadata/Scene/NonModelAssets.tsx b/client/src/pages/Ingestion/components/Metadata/Scene/NonModelAssets.tsx new file mode 100644 index 000000000..be5314fee --- /dev/null +++ b/client/src/pages/Ingestion/components/Metadata/Scene/NonModelAssets.tsx @@ -0,0 +1,163 @@ +/* eslint-disable react/jsx-max-props-per-line */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/** + * NonModelAssets + * + * This component renders the referenced non-model assets for Scene metadata ingestion component. + * The list also provides links to allow individual ingestion/update of assets depending + * on whether they exist in system or not. + */ +import { Box, Typography } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import clsx from 'clsx'; +import React from 'react'; +import { formatBytes } from '../../../../../utils/upload'; + +const useStyles = makeStyles(({ palette, breakpoints }) => ({ + container: { + display: 'flex', + [breakpoints.up('lg')]: { + width: '60vw' + }, + [breakpoints.only('md')]: { + width: '54vw' + }, + minWidth: '880px', + maxWidth: '1100px', + flexDirection: 'column', + borderRadius: 5, + padding: '10px 10px 0px 10px', + backgroundColor: palette.primary.light, + marginBottom: 10 + }, + list: { + padding: 10, + paddingBottom: 0, + marginBottom: 10, + borderRadius: 5, + backgroundColor: palette.secondary.light + }, + header: { + fontSize: '0.8em', + color: palette.primary.dark + }, + label: { + fontSize: '0.8em', + color: palette.primary.dark + }, + labelUnderline: { + textDecoration: 'underline', + cursor: 'pointer' + }, + labelItalics: { + fontStyle: 'italic' + }, + empty: { + textAlign: 'center', + margin: '15px 0px', + fontSize: '0.8em', + color: palette.primary.dark + } +})); + +interface NonModelAsset { + uri: string; + type: string; + description?: string | undefined; + size?: number | undefined; + idAssetVersion?: number | undefined; +} + +interface NonModelAssetsProps { + nonModelAssets: NonModelAsset[]; + idAssetVersion?: number | null; +} + +interface NonModelAssetProps { + nonModelAsset: NonModelAsset; + idAssetVersion?: number | null; +} + +function NonModelAssets(props: NonModelAssetsProps): React.ReactElement { + const { nonModelAssets, idAssetVersion } = props; + const classes = useStyles(); + const hasAssets = !!(nonModelAssets?.length ?? 0); + + return ( + +
+ {!hasAssets && } + {hasAssets && ( + + {nonModelAssets.map((nonModelAsset, index: number) => ( + + ))} + + )} + + ); +} + +function Header(): React.ReactElement { + const classes = useStyles(); + + return ( + + + Referenced Non-Model Assets + + + Type + + + File Size + + + Description + + + Action + + + ); +} + +function Item(props: NonModelAssetProps): React.ReactElement { + const { nonModelAsset } = props; + const { uri, type, description, size, idAssetVersion } = nonModelAsset; + const classes = useStyles(); + + const isAssetInUpload = idAssetVersion === -1; + + return ( + + + {isAssetInUpload && {uri}} + {!isAssetInUpload && {uri}} + + + + {type} + + + {size ? formatBytes(size): ''} + + + {description} + + + {isAssetInUpload && ((uploaded))} + {!isAssetInUpload && (Missing)} + + + ); +} + +function Empty(): React.ReactElement { + const classes = useStyles(); + + return No referenced non-model assets found; +} + +export default NonModelAssets; diff --git a/client/src/pages/Ingestion/components/Metadata/Scene/ReferenceModels.tsx b/client/src/pages/Ingestion/components/Metadata/Scene/ReferenceModels.tsx index cad70df19..6449179d1 100644 --- a/client/src/pages/Ingestion/components/Metadata/Scene/ReferenceModels.tsx +++ b/client/src/pages/Ingestion/components/Metadata/Scene/ReferenceModels.tsx @@ -125,25 +125,25 @@ function Header(): React.ReactElement { return ( - - Reference Models + + Referenced Models - + Usage - + Quality - + File Size - - UV Resolution + + UV - + Bounding Box - + Action @@ -187,6 +187,7 @@ function Item(props: ReferenceModelItemProps): React.ReactElement { getModelDetails(); }, [idAsset, idAssetVersion, idModel, idSystemObject]); + const isModelInUpload = idModel === -1; const isModelInSystem = idModel > 0; let boundingBox: string = ''; @@ -197,13 +198,14 @@ function Item(props: ReferenceModelItemProps): React.ReactElement { return ( - - {isModelInSystem && idSystemObject && ( + + {isModelInUpload && {Name}} + {!isModelInUpload && isModelInSystem && idSystemObject && ( {Name} )} - {!isModelInSystem && {Name}} + {!isModelInUpload && !isModelInSystem && {Name}} @@ -219,16 +221,19 @@ function Item(props: ReferenceModelItemProps): React.ReactElement { {UVResolution} - + {boundingBox} - - {!isModelInSystem && ( + + {isModelInUpload && ( + (uploaded) + )} + {!isModelInUpload && !isModelInSystem && ( Ingest )} - {isModelInSystem && ( + {!isModelInUpload && isModelInSystem && ( Update @@ -241,7 +246,7 @@ function Item(props: ReferenceModelItemProps): React.ReactElement { function Empty(): React.ReactElement { const classes = useStyles(); - return No reference model(s) found; + return No referenced models found; } export default ReferenceModels; diff --git a/client/src/pages/Ingestion/components/Metadata/Scene/SceneDataForm.tsx b/client/src/pages/Ingestion/components/Metadata/Scene/SceneDataForm.tsx index 5672617c4..1c5efe769 100644 --- a/client/src/pages/Ingestion/components/Metadata/Scene/SceneDataForm.tsx +++ b/client/src/pages/Ingestion/components/Metadata/Scene/SceneDataForm.tsx @@ -141,7 +141,6 @@ function SceneDataForm(props: SceneDataProps): React.ReactElement { onChange={setCheckboxField} checked={posedAndQCd} disabled={!canBeQCd} - title='posedAndQCd-input' size='small' color='primary' /> diff --git a/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx b/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx index 2537d1ea4..3429f78f6 100644 --- a/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx +++ b/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx @@ -10,6 +10,7 @@ import { AssetIdentifiers } from '../../../../../components'; import { StateIdentifier, useMetadataStore, StateRelatedObject, useRepositoryStore, useSubjectStore } from '../../../../../store'; import { MetadataType } from '../../../../../store/metadata'; import ReferenceModels from './ReferenceModels'; +import NonModelAssets from './NonModelAssets'; import SceneDataForm from './SceneDataForm'; import { apolloClient } from '../../../../../graphql/index'; import { GetSceneForAssetVersionDocument, RelatedObjectType, useGetSubjectQuery } from '../../../../../types/graphql'; @@ -60,6 +61,18 @@ function Scene(props: SceneProps): React.ReactElement { idScene: 0 } ]); + + // state responsible for non-model assets + const [nonModelAssets, setNonModelAssets] = useState([ + { + uri: '', + type: '', + description: undefined, + size: undefined, + idAssetVersion: undefined, + } + ]); + // state responsible for SceneDataForm const [sceneData, setSceneData] = useState({ idScene: 0, @@ -101,11 +114,18 @@ function Scene(props: SceneProps): React.ReactElement { } } }); + // console.log(`Scene Metadata MSX: ${JSON.stringify(data.getSceneForAssetVersion?.SceneConstellation?.ModelSceneXref)}`); + // console.log(`Scene Metadata Non-Model-Assets: ${JSON.stringify(data.getSceneForAssetVersion?.SceneConstellation?.SvxNonModelAssets)}`); setReferenceModels(data.getSceneForAssetVersion?.SceneConstellation?.ModelSceneXref); + setNonModelAssets(data.getSceneForAssetVersion?.SceneConstellation?.SvxNonModelAssets); setSceneData(data.getSceneForAssetVersion?.SceneConstellation?.Scene); - const invalidMetadataStep = data.getSceneForAssetVersion?.SceneConstellation?.ModelSceneXref.some(reference => reference.idModel === 0); + + const missingModels = data.getSceneForAssetVersion?.SceneConstellation?.ModelSceneXref.some(reference => reference.idModel === 0); + const missingNonModelAssets = data.getSceneForAssetVersion?.SceneConstellation?.SvxNonModelAssets.some(reference => (reference.idAssetVersion ?? 0) === 0); + const invalidMetadataStep: boolean = missingModels || missingNonModelAssets; setInvalidMetadataStep(invalidMetadataStep); - if (invalidMetadataStep) toast.warning('Unable to ingest scene because reference models cannot be found', { autoClose: false }); + if (invalidMetadataStep) + toast.warning('Unable to ingest scene because some or all referenced assets cannot be found', { autoClose: false }); } fetchSceneConstellation(); @@ -215,6 +235,7 @@ function Scene(props: SceneProps): React.ReactElement { /> + )} ; }; +export type SvxNonModelAsset = { + __typename?: 'SvxNonModelAsset'; + uri: Scalars['String']; + type: Scalars['String']; + description?: Maybe; + size?: Maybe; + idAssetVersion?: Maybe; +}; + export type SceneConstellation = { __typename?: 'SceneConstellation'; Scene?: Maybe; ModelSceneXref?: Maybe>>; + SvxNonModelAssets?: Maybe>; }; export type UpdateObjectDetailsInput = { @@ -3568,7 +3578,10 @@ export type GetSceneForAssetVersionQuery = ( & Pick )> } )> } - )>>> } + )>>>, SvxNonModelAssets?: Maybe + )>> } )> } ) } ); @@ -6204,6 +6217,13 @@ export const GetSceneForAssetVersionDocument = gql` } } } + SvxNonModelAssets { + uri + type + description + size + idAssetVersion + } } } } diff --git a/server/db/api/composite/SceneConstellation.ts b/server/db/api/composite/SceneConstellation.ts index d3830a79d..7bea336b2 100644 --- a/server/db/api/composite/SceneConstellation.ts +++ b/server/db/api/composite/SceneConstellation.ts @@ -1,22 +1,25 @@ -import { Scene, Model, ModelSceneXref, Vocabulary } from '../..'; +import { Scene, Model, ModelSceneXref, AssetVersion, Vocabulary, SystemObjectInfo } from '../..'; import * as H from '../../../utils/helpers'; import * as LOG from '../../../utils/logger'; import * as STORE from '../../../storage/interface'; import * as CACHE from '../../../cache'; import * as COMMON from '@dpo-packrat/common'; import { IZip } from '../../../utils/IZip'; -import { SvxReader } from '../../../utils/parser/svxReader'; +import { SvxReader, SvxNonModelAsset } from '../../../utils/parser/svxReader'; +import * as path from 'path'; export class SceneConstellation { Scene: Scene | null; ModelSceneXref: ModelSceneXref[] | null; + SvxNonModelAssets: SvxNonModelAsset[] | null; private static vocabAssetTypeModel: Vocabulary | undefined = undefined; private static vocabAssetTypeModelGeometryFile: Vocabulary | undefined = undefined; - private constructor(Scene: Scene, ModelSceneXref: ModelSceneXref[] | null) { + private constructor(Scene: Scene, ModelSceneXref: ModelSceneXref[] | null, SvxNonModelAssets: SvxNonModelAsset[] | null) { this.Scene = Scene; this.ModelSceneXref = ModelSceneXref; + this.SvxNonModelAssets = SvxNonModelAssets; } static async fetchFromScene(idScene: number): Promise { @@ -30,7 +33,61 @@ export class SceneConstellation { LOG.error(`SceneConstellation.fetchFromScene(${idScene}) failed to retrieve ModelSceneXref`, LOG.LS.eDB); return null; } - return new SceneConstellation(scene, modelSceneXref); + + const handledAssetSet: Set = new Set(); + for (const MSX of modelSceneXref) { + if (MSX.Name) + handledAssetSet.add(MSX.Name?.toLowerCase()); + } + + // Compute non Model Assets + let SvxNonModelAssets: SvxNonModelAsset[] | null = null; + + const sOI: SystemObjectInfo | undefined = await CACHE.SystemObjectCache.getSystemFromScene(scene); + if (sOI) + SvxNonModelAssets = await SceneConstellation.computeNonModelAssets(sOI.idSystemObject, handledAssetSet); + else + LOG.error(`SceneConstellation.fetchFromScene(${idScene}) failed to compute scene's idSystemObject`, LOG.LS.eDB); + + return new SceneConstellation(scene, modelSceneXref, SvxNonModelAssets); + } + + private static async computeNonModelAssets(idSystemObject: number, handledAssetSet?: Set | undefined): Promise { + const assetVersions: AssetVersion[] | null = await AssetVersion.fetchLatestFromSystemObject(idSystemObject); + if (!assetVersions) { + LOG.error(`SceneConstellation.computeNonModelAssets(${idSystemObject}) failed to compute scene's asset versions`, LOG.LS.eDB); + return null; + } + + if (!handledAssetSet) + handledAssetSet = new Set(); + + const SvxNonModelAssets: SvxNonModelAsset[] = []; + for (const assetVersion of assetVersions) { + const normalizedName: string = assetVersion.FileName.toLowerCase(); + if (handledAssetSet.has(normalizedName)) + continue; + + handledAssetSet.add(normalizedName); + + let type: string | null = null; + const extension: string = path.extname(assetVersion.FileName).toLowerCase() || assetVersion.FileName.toLowerCase(); + switch (extension) { + case '.jpg': + case '.jpeg': + type = 'Image'; + break; + + case '.html': + case '.htm': + type = 'Article'; + break; + } + + if (type) + SvxNonModelAssets.push({ uri: (assetVersion.FilePath ? assetVersion.FilePath + '/' : '') + assetVersion.FileName, type, idAssetVersion: assetVersion.idAssetVersion }); + } + return SvxNonModelAssets; } static async fetchFromAssetVersion(idAssetVersion: number, directory?: string | undefined, idScene?: number | undefined): Promise { @@ -58,7 +115,7 @@ export class SceneConstellation { isBagit = CAR.isBagit; const files: string[] = await SceneConstellation.fetchFileFromZip(zip, isBagit, '.svx.json', directory) ?? []; - if (files.length == 0) { + if (files.length === 0) { LOG.error(`SceneConstellation.fetchFromAssetVersion unable to locate scene file with .svx.json extension in zipfile ${RSR.fileName} for idAssetVersion ${idAssetVersion}`, LOG.LS.eDB); return null; } else @@ -84,6 +141,7 @@ export class SceneConstellation { // If we have a source scene, compute a mapping of model names to idModels, so we can specify the correct ModelSceneXref below const modelExistingNameMap: Map = new Map(); + const assetExistingNameMap: Map = new Map(); if (idScene) { const modelSceneXrefs: ModelSceneXref[] | null = await ModelSceneXref.fetchFromScene(idScene); if (modelSceneXrefs) { @@ -96,6 +154,18 @@ export class SceneConstellation { } } else LOG.error(`SceneConstellation.fetchFromAssetVersion unable to read original ModelSceneXref from idScene ${idScene} for idAssetVersion ${idAssetVersion}`, LOG.LS.eDB); + + const sOI: SystemObjectInfo | undefined = await CACHE.SystemObjectCache.getSystemFromObjectID({ idObject: idScene, eObjectType: COMMON.eSystemObjectType.eScene }); + if (sOI) { + const SvxNonModelAsset: SvxNonModelAsset[] | null = await SceneConstellation.computeNonModelAssets(sOI.idSystemObject); + if (SvxNonModelAsset) { + for (const NMA of SvxNonModelAsset) + assetExistingNameMap.set(NMA.uri, NMA.idAssetVersion); + LOG.info(`SceneConstellation.fetchFromAssetVersion assetExistingNameMap=${H.Helpers.JSONStringify(assetExistingNameMap)}`, LOG.LS.eDB); + } else + LOG.error(`SceneConstellation.fetchFromAssetVersion unable to compute non-model assets from idScene ${idScene} for idAssetVersion ${idAssetVersion}`, LOG.LS.eDB); + } else + LOG.error(`SceneConstellation.fetchFromAssetVersion unable to compute system object info from idScene ${idScene} for idAssetVersion ${idAssetVersion}`, LOG.LS.eDB); } const scene: Scene = svx.SvxExtraction.extractScene(); @@ -116,7 +186,7 @@ export class SceneConstellation { // if we have a zip, look for our model within that zip by name if (zip) { const files: string[] = await SceneConstellation.fetchFileFromZip(zip, isBagit, MSX.Name, directory) ?? []; - if (files.length == 1) { // found it ... record it as found but not ingested (i.e. MSX.idModel === -1) + if (files.length === 1) { // found it ... record it as found but not ingested (i.e. MSX.idModel === -1) MSX.idModel = -1; // non-zero value, but invalid... modelSceneXrefs.push(MSX); continue; @@ -136,8 +206,32 @@ export class SceneConstellation { MSX.idModel = idModel; modelSceneXrefs.push(MSX); } + + } + + let nonModelAssets: SvxNonModelAsset[] | null = null; + if (svx.SvxExtraction.nonModelAssets) { + nonModelAssets = []; + for (const NMA of svx.SvxExtraction.nonModelAssets) { + LOG.info(`SceneConstellation.fetchFromAssetVersion processing nonModelAsset ${H.Helpers.JSONStringify(NMA)}`, LOG.LS.eDB); + // if we have a zip, look for our asset within that zip by name + if (zip) { + const files: string[] = await SceneConstellation.fetchFileFromZip(zip, isBagit, NMA.uri, directory) ?? []; + if (files.length === 1) { // found it ... record it as found but not ingested (i.e. MSX.idModel === -1) + NMA.idAssetVersion = -1; // non-zero value, but invalid... + nonModelAssets.push(NMA); + continue; + } + } + + const idAssetVersion: number | undefined = assetExistingNameMap.get(NMA.uri); + if (idAssetVersion) + NMA.idAssetVersion = idAssetVersion; + nonModelAssets.push(NMA); + } } - return new SceneConstellation(scene, modelSceneXrefs); + // LOG.info(`SceneConstellation.fetchFromAssetVersion scene=${H.Helpers.JSONStringify(scene)}\nmodelSceneXrefs=${H.Helpers.JSONStringify(modelSceneXrefs)}\nnonModelAssets=${H.Helpers.JSONStringify(nonModelAssets)}`, LOG.LS.eDB); + return new SceneConstellation(scene, modelSceneXrefs, nonModelAssets); } finally { if (zip) await zip.close(); diff --git a/server/graphql/api/queries/scene/getSceneForAssetVersion.ts b/server/graphql/api/queries/scene/getSceneForAssetVersion.ts index 93fa1c8b3..4b59afe1f 100644 --- a/server/graphql/api/queries/scene/getSceneForAssetVersion.ts +++ b/server/graphql/api/queries/scene/getSceneForAssetVersion.ts @@ -42,6 +42,13 @@ const getSceneForAssetVersion = gql` } } } + SvxNonModelAssets { + uri + type + description + size + idAssetVersion + } } } } diff --git a/server/graphql/schema.graphql b/server/graphql/schema.graphql index 97c347485..3bf51882e 100644 --- a/server/graphql/schema.graphql +++ b/server/graphql/schema.graphql @@ -1040,9 +1040,18 @@ type IntermediaryFile { SystemObject: SystemObject } +type SvxNonModelAsset { + uri: String! + type: String! + description: String + size: Int + idAssetVersion: Int +} + type SceneConstellation { Scene: Scene ModelSceneXref: [ModelSceneXref] + SvxNonModelAssets: [SvxNonModelAsset!] } input UpdateObjectDetailsInput { diff --git a/server/graphql/schema/scene/types.graphql b/server/graphql/schema/scene/types.graphql index 77c3a3953..6cea08840 100644 --- a/server/graphql/schema/scene/types.graphql +++ b/server/graphql/schema/scene/types.graphql @@ -38,7 +38,16 @@ type IntermediaryFile { SystemObject: SystemObject } +type SvxNonModelAsset { + uri: String! + type: String! + description: String + size: Int + idAssetVersion: Int +} + type SceneConstellation { Scene: Scene ModelSceneXref: [ModelSceneXref] + SvxNonModelAssets: [SvxNonModelAsset!] } diff --git a/server/types/graphql.ts b/server/types/graphql.ts index b74b6905a..59c6d7bff 100644 --- a/server/types/graphql.ts +++ b/server/types/graphql.ts @@ -1486,10 +1486,20 @@ export type IntermediaryFile = { SystemObject?: Maybe; }; +export type SvxNonModelAsset = { + __typename?: 'SvxNonModelAsset'; + uri: Scalars['String']; + type: Scalars['String']; + description?: Maybe; + size?: Maybe; + idAssetVersion?: Maybe; +}; + export type SceneConstellation = { __typename?: 'SceneConstellation'; Scene?: Maybe; ModelSceneXref?: Maybe>>; + SvxNonModelAssets?: Maybe>; }; export type UpdateObjectDetailsInput = { diff --git a/server/utils/parser/svxReader.ts b/server/utils/parser/svxReader.ts index 2c912d8b1..6030ea655 100644 --- a/server/utils/parser/svxReader.ts +++ b/server/utils/parser/svxReader.ts @@ -5,6 +5,14 @@ import * as H from '../helpers'; import * as SVX from '../../types/voyager'; import * as THREE from 'three'; +export type SvxNonModelAsset = { + uri: string; + type: string; + description?: string | undefined; + size?: number | undefined; + idAssetVersion?: number | undefined; +}; + /** Create instances using the static SvxExtraction.extract() */ export class SvxExtraction { document: SVX.IDocument; @@ -18,6 +26,10 @@ export class SvxExtraction { setupCount: number = 0; tourCount: number = 0; + metaImages: SVX.IImage[] | null = null; + metaArticles: SVX.IArticle[] | null = null; + nonModelAssets: SvxNonModelAsset[] | null = null; + extractScene(): DBAPI.Scene { // first, attempt to extract Name and Title from metas -> collection -> title, sceneTitle let Name: string = ''; @@ -154,18 +166,62 @@ export class SvxExtraction { } } for (const derivative of model.derivatives) { - for (const asset of derivative.assets) { - const xref: DBAPI.ModelSceneXref = new DBAPI.ModelSceneXref({ - idModelSceneXref: 0, idModel: 0, idScene: 0, Name: asset.uri, Usage: derivative.usage, Quality: derivative.quality, - FileSize: asset.byteSize !== undefined ? BigInt(asset.byteSize) : null, UVResolution: asset.imageSize || null, - BoundingBoxP1X, BoundingBoxP1Y, BoundingBoxP1Z, BoundingBoxP2X, BoundingBoxP2Y, BoundingBoxP2Z, - TS0, TS1, TS2, R0, R1, R2, R3, S0, S1, S2 - }); + if (derivative.usage !== 'Image2D') { // Skip image derivatives here + for (const asset of derivative.assets) { + const xref: DBAPI.ModelSceneXref = new DBAPI.ModelSceneXref({ + idModelSceneXref: 0, idModel: 0, idScene: 0, Name: asset.uri, Usage: derivative.usage, Quality: derivative.quality, + FileSize: asset.byteSize !== undefined ? BigInt(asset.byteSize) : null, UVResolution: asset.imageSize || null, + BoundingBoxP1X, BoundingBoxP1Y, BoundingBoxP1Z, BoundingBoxP2X, BoundingBoxP2Y, BoundingBoxP2Z, + TS0, TS1, TS2, R0, R1, R2, R3, S0, S1, S2 + }); + + this.modelDetails.push(xref); + } + } + } + } + return true; + } + + private extractNonModelDetails(): boolean { + const metaImages: SVX.IImage[] = []; + const metaArticles: SVX.IArticle[] = []; + const nonModelAssets: SvxNonModelAsset[] = []; - this.modelDetails.push(xref); + if (this.document.metas) { + for (const meta of this.document.metas) { + if (meta.images) { + for (const image of meta.images) { + metaImages.push(image); + nonModelAssets.push({ uri: image.uri, type: 'Image', description: image.quality, size: image.byteSize }); + } + } + + if (meta.articles) { + for (const article of meta.articles) { + metaArticles.push(article); + if (article.uri) + nonModelAssets.push({ uri: article.uri, type: 'Article', description: article.title }); + else if (article.uris) { + for (const [lang, uri] of Object.entries(article.uris)) { + let description: string | undefined = article.titles ? article.titles[lang] : undefined; + if (description === undefined) + description = article.title; + nonModelAssets.push({ uri, type: 'Article', description }); + } + } + } } } } + + if (metaImages.length > 0) + this.metaImages = metaImages; + if (metaArticles.length > 0) + this.metaArticles = metaArticles; + if (nonModelAssets.length > 0) + this.nonModelAssets = nonModelAssets; + return true; } @@ -212,6 +268,7 @@ export class SvxExtraction { } svx.tourCount = tourCount; svx.extractModelDetails(); + svx.extractNonModelDetails(); return { svx, results: { success: true } }; } } From 17b15fe124568c2a0c69f011005e73a699258f97 Mon Sep 17 00:00:00 2001 From: Jon Tyson <6943745+jahjedtieson@users.noreply.github.com> Date: Fri, 25 Mar 2022 14:57:02 -0700 Subject: [PATCH 08/31] Client: * Avoid crash when getSceneForAssetVersion does not return ModelSceneXref or SvxNonModelAssets --- .../Ingestion/components/Metadata/Scene/index.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx b/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx index 3429f78f6..371b8112a 100644 --- a/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx +++ b/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /** * Metadata - Scene * @@ -116,12 +117,15 @@ function Scene(props: SceneProps): React.ReactElement { }); // console.log(`Scene Metadata MSX: ${JSON.stringify(data.getSceneForAssetVersion?.SceneConstellation?.ModelSceneXref)}`); // console.log(`Scene Metadata Non-Model-Assets: ${JSON.stringify(data.getSceneForAssetVersion?.SceneConstellation?.SvxNonModelAssets)}`); - setReferenceModels(data.getSceneForAssetVersion?.SceneConstellation?.ModelSceneXref); - setNonModelAssets(data.getSceneForAssetVersion?.SceneConstellation?.SvxNonModelAssets); + const ModelSceneXref: any = data.getSceneForAssetVersion?.SceneConstellation?.ModelSceneXref; + const SvxNonModelAssets: any = data.getSceneForAssetVersion?.SceneConstellation?.SvxNonModelAssets; + + setReferenceModels(ModelSceneXref); + setNonModelAssets(SvxNonModelAssets); setSceneData(data.getSceneForAssetVersion?.SceneConstellation?.Scene); - const missingModels = data.getSceneForAssetVersion?.SceneConstellation?.ModelSceneXref.some(reference => reference.idModel === 0); - const missingNonModelAssets = data.getSceneForAssetVersion?.SceneConstellation?.SvxNonModelAssets.some(reference => (reference.idAssetVersion ?? 0) === 0); + const missingModels: boolean = ModelSceneXref ? ModelSceneXref.some(reference => reference.idModel === 0) : false; + const missingNonModelAssets: boolean = SvxNonModelAssets ? SvxNonModelAssets.some(reference => (reference.idAssetVersion ?? 0) === 0) : false; const invalidMetadataStep: boolean = missingModels || missingNonModelAssets; setInvalidMetadataStep(invalidMetadataStep); if (invalidMetadataStep) From b1eee26a1fa77b872effbbd7fd44e98ee429f104 Mon Sep 17 00:00:00 2001 From: Hsin Tung Date: Fri, 25 Mar 2022 17:06:43 -0700 Subject: [PATCH 09/31] WIP updating details view to include subtitle control --- .../shared/DataGridWithPagination.tsx | 10 +- .../pages/Admin/components/AddProjectForm.tsx | 2 +- .../Admin/components/AdminProjectsView.tsx | 6 +- .../pages/Admin/components/AdminUnitsView.tsx | 6 +- .../Admin/components/AdminUsersFilter.tsx | 4 +- .../pages/Admin/components/AdminUsersList.tsx | 2 +- .../Admin/components/License/LicenseView.tsx | 6 +- .../Metadata/Control/SubtitleControl.tsx | 158 ++++++++++++++ .../components/Metadata/Model/index.tsx | 60 +++-- .../Metadata/Scene/SceneDataForm.tsx | 25 +-- .../components/Metadata/Scene/index.tsx | 86 +++++++- .../Ingestion/components/Metadata/index.tsx | 18 +- .../components/SubjectItem/ItemList.tsx | 206 ++++++++++++------ .../components/SubjectItem/ProjectList.tsx | 45 ---- .../components/SubjectItem/index.tsx | 52 ++--- client/src/pages/Ingestion/hooks/useIngest.ts | 35 ++- .../DetailsView/DetailsTab/ItemDetails.tsx | 3 +- .../DetailsView/DetailsTab/SubjectDetails.tsx | 67 ++++-- .../components/DetailsView/index.tsx | 4 +- client/src/store/index.ts | 1 - client/src/store/item.ts | 178 +++++++++++---- client/src/store/metadata/index.ts | 64 ++++-- .../src/store/metadata/metadata.defaults.ts | 61 +++++- client/src/store/metadata/metadata.types.ts | 18 +- client/src/store/project.ts | 79 ------- client/src/store/repository.ts | 3 +- client/src/store/subject.ts | 93 +------- client/src/store/utils.ts | 58 ++++- client/src/types/graphql.tsx | 142 ------------ server/graphql/api/index.ts | 42 ++-- .../unit/getIngestionItemsForSubjects.ts | 15 -- .../unit/getIngestionProjectsForSubjects.ts | 15 -- server/graphql/schema.graphql | 19 -- .../resolvers/mutations/ingestData.ts | 3 + server/graphql/schema/unit/queries.graphql | 19 -- server/graphql/schema/unit/resolvers/index.ts | 4 - .../resolvers/queries/getIngestionItems.ts | 2 +- .../queries/getIngestionItemsForSubjects.ts | 11 - .../getIngestionProjectsForSubjects.ts | 33 --- server/tests/graphql/graphql.test.ts | 4 - .../mutations/ingestion/ingestData.test.ts | 18 +- .../unit/getIngestionItemsForSubjects.test.ts | 37 ---- .../getIngestionProjectsForSubjects.test.ts | 37 ---- server/types/graphql.ts | 31 --- server/utils/nameHelpers.ts | 8 +- 45 files changed, 871 insertions(+), 919 deletions(-) create mode 100644 client/src/pages/Ingestion/components/Metadata/Control/SubtitleControl.tsx delete mode 100644 client/src/pages/Ingestion/components/SubjectItem/ProjectList.tsx delete mode 100644 client/src/store/project.ts delete mode 100644 server/graphql/api/queries/unit/getIngestionItemsForSubjects.ts delete mode 100644 server/graphql/api/queries/unit/getIngestionProjectsForSubjects.ts delete mode 100644 server/graphql/schema/unit/resolvers/queries/getIngestionItemsForSubjects.ts delete mode 100644 server/graphql/schema/unit/resolvers/queries/getIngestionProjectsForSubjects.ts delete mode 100644 server/tests/graphql/queries/unit/getIngestionItemsForSubjects.test.ts delete mode 100644 server/tests/graphql/queries/unit/getIngestionProjectsForSubjects.test.ts diff --git a/client/src/components/shared/DataGridWithPagination.tsx b/client/src/components/shared/DataGridWithPagination.tsx index 4614e3436..51dc3b185 100644 --- a/client/src/components/shared/DataGridWithPagination.tsx +++ b/client/src/components/shared/DataGridWithPagination.tsx @@ -13,12 +13,10 @@ const useStyles = makeStyles(({ palette, typography }) => ({ flexDirection: 'column', overflow: 'auto', maxHeight: 'calc(100vh - 60px)', - paddingLeft: '1%', - width: '1200px', - margin: '0 auto' + width: '1200px' }, DataGridContainer: { - marginTop: '2%', + marginTop: '20px', width: '1000px', padding: '20px', height: 'calc(100% - 120px)', @@ -49,11 +47,9 @@ const useStyles = makeStyles(({ palette, typography }) => ({ searchFilterContainer: { display: 'flex', justifyContent: 'space-around', - height: '70px', width: 'fit-content', backgroundColor: '#FFFCD1', - paddingLeft: '20px', - paddingRight: '20px' + padding: '15px 20px' }, searchFilterSettingsContainer: { display: 'flex', diff --git a/client/src/pages/Admin/components/AddProjectForm.tsx b/client/src/pages/Admin/components/AddProjectForm.tsx index 4f1db2f54..59475307e 100644 --- a/client/src/pages/Admin/components/AddProjectForm.tsx +++ b/client/src/pages/Admin/components/AddProjectForm.tsx @@ -169,7 +169,7 @@ function AddProjectForm(): React.ReactElement { Description: description } }, - refetchQueries: ['getProjectList', 'getIngestionProjectsForSubjects'] + refetchQueries: ['getProjectList'] }); if (data?.createProject) { toast.success('Project created successfully'); diff --git a/client/src/pages/Admin/components/AdminProjectsView.tsx b/client/src/pages/Admin/components/AdminProjectsView.tsx index b9612cfef..5e7ee50d1 100644 --- a/client/src/pages/Admin/components/AdminProjectsView.tsx +++ b/client/src/pages/Admin/components/AdminProjectsView.tsx @@ -19,7 +19,7 @@ import Clear from '@material-ui/icons/Clear'; const useStyles = makeStyles({ AdminListContainer: { - marginTop: '2%', + marginTop: '20px', width: '450px', padding: '20px', height: 'calc(100% - 120px)', @@ -71,11 +71,9 @@ const useStyles = makeStyles({ AdminSearchFilterContainer: { display: 'flex', justifyContent: 'space-around', - height: '70px', width: '600px', backgroundColor: '#FFFCD1', - paddingLeft: '20px', - paddingRight: '20px' + padding: '15px 20px' }, AdminUsersSearchFilterSettingsContainer: { display: 'flex', diff --git a/client/src/pages/Admin/components/AdminUnitsView.tsx b/client/src/pages/Admin/components/AdminUnitsView.tsx index 1c0b47062..d0f7f998d 100644 --- a/client/src/pages/Admin/components/AdminUnitsView.tsx +++ b/client/src/pages/Admin/components/AdminUnitsView.tsx @@ -19,7 +19,7 @@ import Clear from '@material-ui/icons/Clear'; const useStyles = makeStyles({ AdminListContainer: { - marginTop: '2%', + marginTop: '20px', width: '80%', padding: '20px', height: 'calc(100% - 120px)', @@ -71,11 +71,9 @@ const useStyles = makeStyles({ AdminSearchFilterContainer: { display: 'flex', justifyContent: 'space-around', - height: '70px', width: '600px', backgroundColor: '#FFFCD1', - paddingLeft: '20px', - paddingRight: '20px' + padding: '15px 20px' }, AdminUsersSearchFilterSettingsContainer: { display: 'flex', diff --git a/client/src/pages/Admin/components/AdminUsersFilter.tsx b/client/src/pages/Admin/components/AdminUsersFilter.tsx index a21aba42e..aef3021bf 100644 --- a/client/src/pages/Admin/components/AdminUsersFilter.tsx +++ b/client/src/pages/Admin/components/AdminUsersFilter.tsx @@ -21,11 +21,9 @@ const useStyles = makeStyles(({ typography, palette }) => ({ AdminUsersSearchFilterContainer: { display: 'flex', justifyContent: 'space-around', - height: '70px', width: '900px', backgroundColor: '#FFFCD1', - paddingLeft: '20px', - paddingRight: '20px' + padding: '15px 20px' }, AdminUsersSearchFilterSettingsContainer: { display: 'flex', diff --git a/client/src/pages/Admin/components/AdminUsersList.tsx b/client/src/pages/Admin/components/AdminUsersList.tsx index 051a38ce4..597267300 100644 --- a/client/src/pages/Admin/components/AdminUsersList.tsx +++ b/client/src/pages/Admin/components/AdminUsersList.tsx @@ -14,7 +14,7 @@ import { extractISOMonthDateYear } from '../../../constants/index'; const useStyles = makeStyles({ AdminUsersListContainer: { - marginTop: '2%', + marginTop: '20px', width: '1000px', padding: '20px', height: 'calc(100% - 120px)', diff --git a/client/src/pages/Admin/components/License/LicenseView.tsx b/client/src/pages/Admin/components/License/LicenseView.tsx index 4785155fd..7dc3d417b 100644 --- a/client/src/pages/Admin/components/License/LicenseView.tsx +++ b/client/src/pages/Admin/components/License/LicenseView.tsx @@ -20,7 +20,7 @@ import Clear from '@material-ui/icons/Clear'; const useStyles = makeStyles({ AdminListContainer: { - marginTop: '2%', + marginTop: '20px', width: '80%', padding: '20px', height: 'calc(100% - 120px)', @@ -72,11 +72,9 @@ const useStyles = makeStyles({ AdminSearchFilterContainer: { display: 'flex', justifyContent: 'space-around', - height: '70px', width: '600px', backgroundColor: '#FFFCD1', - paddingLeft: '20px', - paddingRight: '20px' + padding: '15px 20px' }, AdminUsersSearchFilterSettingsContainer: { display: 'flex', diff --git a/client/src/pages/Ingestion/components/Metadata/Control/SubtitleControl.tsx b/client/src/pages/Ingestion/components/Metadata/Control/SubtitleControl.tsx new file mode 100644 index 000000000..c0175fa45 --- /dev/null +++ b/client/src/pages/Ingestion/components/Metadata/Control/SubtitleControl.tsx @@ -0,0 +1,158 @@ +import React from 'react'; +import { SubtitleFields, eSubtitleOption } from '../../../../../store/metadata/metadata.types'; +import { Box, makeStyles, Typography, Table, TableBody, TableCell, TableContainer, TableRow, fade } from '@material-ui/core' +import { RiCheckboxBlankCircleLine, RiRecordCircleFill } from 'react-icons/ri'; +import { grey } from '@material-ui/core/colors'; +import { palette } from '../../../../../theme'; +import { DebounceInput } from 'react-debounce-input'; +import clsx from 'clsx'; + +interface SubtitleControlProps { + subtitles: SubtitleFields; + objectName: string; + onSelectSubtitle: (id: number) => void; + onUpdateCustomSubtitle: (event: React.ChangeEvent, id: number) => void; + hasPrimaryTheme: boolean +} + +const useStyles = makeStyles(({ palette, typography }) => ({ + container: { + display: 'flex', + flexDirection: 'column', + backgroundColor: (hasPrimaryTheme) => hasPrimaryTheme ? palette.primary.light : palette.secondary.light, + width: 'fit-content', + minWidth: 300 + }, + selected: { + cursor: 'pointer', + }, + cell: { + border: 'none', + padding: '1px 10px', + height: 30 + }, + labelCell: { + width: 50 + }, + optionContainer: { + display: 'flex', + padding: '0 16px 0 0', + alignItems: 'center' + }, + input: { + height: 18, + border: `1px solid ${fade(palette.primary.contrastText, 0.4)}`, + fontFamily: typography.fontFamily, + fontSize: '0.8rem', + padding: '0px 10px', + borderRadius: 5, + }, + text: { + fontSize: '0.75rem' + } +})) + +function SubtitleControl(props: SubtitleControlProps): React.ReactElement { + const { objectName, subtitles, onUpdateCustomSubtitle, onSelectSubtitle, hasPrimaryTheme } = props; + const classes = useStyles(hasPrimaryTheme); + const selectedSubtitle = subtitles.find(subtitle => subtitle.selected === true)?.value; + const selectedSubtitlesName = selectedSubtitle ? `: ${selectedSubtitle}` : ''; + const sortedSubtitles: SubtitleFields = subtitles.sort((a, b) => a.subtitleOption - b.subtitleOption); + + + const renderSubtitleOptions = (subtitles: SubtitleFields): React.ReactElement => { + // Case: forced + if (subtitles.some(option => option.subtitleOption === eSubtitleOption.eForced)) + return ( + + + Name: + + + {`${objectName}${selectedSubtitlesName}`} + + + ); + + // Case: Name input only + if (subtitles.length === 1 && subtitles.find(option => option.subtitleOption === eSubtitleOption.eInput)) { + const { id, value } = subtitles[0]; + return ( + + + Name: + + + onUpdateCustomSubtitle(e, id)} + element='input' + value={value} + className={classes.input} + debounceTimeout={400} + title={`subtitle-input-${value}`} + /> + + + ); + } + + // Case: mixed + const options = ( + <> + + + Name: + + + {`${objectName}${selectedSubtitlesName}`} + + + + + Subtitle: + + +
+ { + sortedSubtitles.map(({ selected, value, id, subtitleOption }, key) => ( +
+ {!selected && onSelectSubtitle(id)} size={18} color={grey[400]} />} + {selected && onSelectSubtitle(id)} size={18} color={palette.primary.main} />} + { + subtitleOption === eSubtitleOption.eInherit ? {value} + : subtitleOption === eSubtitleOption.eNone ? None + : onUpdateCustomSubtitle(e, id)} + element='input' + value={value} + className={classes.input} + debounceTimeout={400} + title={`subtitle-input-${value}`} + /> + } +
+ )) + } +
+
+
+ + ); + + return {options}; + } + + return ( + + + + + {renderSubtitleOptions(subtitles)} + +
+
+
+ ); +} + +export default SubtitleControl; diff --git a/client/src/pages/Ingestion/components/Metadata/Model/index.tsx b/client/src/pages/Ingestion/components/Metadata/Model/index.tsx index b81887257..58015ac77 100644 --- a/client/src/pages/Ingestion/components/Metadata/Model/index.tsx +++ b/client/src/pages/Ingestion/components/Metadata/Model/index.tsx @@ -8,7 +8,7 @@ */ import { Box, makeStyles, Typography, Table, TableBody, TableCell, TableContainer, TableRow, Paper, Select, MenuItem } from '@material-ui/core'; import React, { useState, useEffect } from 'react'; -import { AssetIdentifiers, DateInputField, /*FieldType, InputField, SelectField,*/ ReadOnlyRow, SidebarBottomNavigator, TextArea } from '../../../../../components'; +import { AssetIdentifiers, DateInputField, ReadOnlyRow, SidebarBottomNavigator, TextArea } from '../../../../../components'; import { StateIdentifier, StateRelatedObject, useSubjectStore, useMetadataStore, useVocabularyStore, useRepositoryStore, FieldErrors } from '../../../../../store'; import { MetadataType } from '../../../../../store/metadata'; import { GetModelConstellationForAssetVersionDocument, RelatedObjectType, useGetSubjectQuery } from '../../../../../types/graphql'; @@ -20,9 +20,11 @@ import AssetFilesTable from './AssetFilesTable'; import { extractModelConstellation } from '../../../../../constants'; import { apolloClient } from '../../../../../graphql/index'; import { useStyles as useTableStyles } from '../../../../Repository/components/DetailsView/DetailsTab/CaptureDataDetails'; -import { DebounceInput } from 'react-debounce-input'; import { errorFieldStyling } from '../Photogrammetry'; +import SubtitleControl from '../Control/SubtitleControl'; import clsx from 'clsx'; +import lodash from 'lodash'; +import { toast } from 'react-toastify'; const useStyles = makeStyles(({ palette }) => ({ container: { @@ -267,6 +269,33 @@ function Model(props: ModelProps): React.ReactElement { onModalClose(); }; + const onSelectSubtitle = (id: number) => { + const updatedSubtitles = model.subtitles.map((subtitle) => { + return { + id: subtitle.id, + value: subtitle.value, + subtitleOption: subtitle.subtitleOption, + selected: id === subtitle.id + } + }); + updateMetadataField(metadataIndex, 'subtitles', updatedSubtitles, MetadataType.model); + // console.log('id', id, updatedSubtitles); + }; + + const onUpdateCustomSubtitle = (event: React.ChangeEvent, id: number) => { + const subtitlesCopy = lodash.cloneDeep(model.subtitles); + const targetSubtitle = subtitlesCopy.find(subtitle => subtitle.id === id); + + if (!targetSubtitle) { + toast.warn('Something went wrong with updating the subtitle. Please try again'); + return; + } + + targetSubtitle.value = event.target.value; + updateMetadataField(metadataIndex, 'subtitles', subtitlesCopy, MetadataType.model); + // console.log('event', event.target.value, id); + }; + return ( @@ -317,26 +346,23 @@ function Model(props: ModelProps): React.ReactElement { Model - + {!idAsset && ( + + + + )} - - Name - - - - + Date Created diff --git a/client/src/pages/Ingestion/components/Metadata/Scene/SceneDataForm.tsx b/client/src/pages/Ingestion/components/Metadata/Scene/SceneDataForm.tsx index 5672617c4..1cd331c8c 100644 --- a/client/src/pages/Ingestion/components/Metadata/Scene/SceneDataForm.tsx +++ b/client/src/pages/Ingestion/components/Metadata/Scene/SceneDataForm.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { Box, Checkbox, Tooltip, Typography, Table, TableBody, TableCell, TableContainer, TableRow, Paper } from '@material-ui/core'; -import { DebounceInput } from 'react-debounce-input'; import { makeStyles, fade } from '@material-ui/core/styles'; import clsx from 'clsx'; @@ -70,18 +69,16 @@ interface SceneData { interface SceneDataProps { sceneData: SceneData; - name: string; EdanUUID: string; approvedForPublication: boolean; posedAndQCd: boolean; canBeQCd: boolean; idAssetVersion?: number; - setNameField: ({ target }: { target: EventTarget }) => void; setCheckboxField: ({ target }: { target: EventTarget }) => void; } function SceneDataForm(props: SceneDataProps): React.ReactElement { - const { sceneData, setCheckboxField, setNameField, name, approvedForPublication, posedAndQCd, canBeQCd, EdanUUID } = props; + const { sceneData, setCheckboxField, approvedForPublication, posedAndQCd, canBeQCd, EdanUUID } = props; const classes = useStyles(); if (!sceneData) return ; @@ -91,24 +88,7 @@ function SceneDataForm(props: SceneDataProps): React.ReactElement {
- - - - Name - - - - - - + @@ -141,7 +121,6 @@ function SceneDataForm(props: SceneDataProps): React.ReactElement { onChange={setCheckboxField} checked={posedAndQCd} disabled={!canBeQCd} - title='posedAndQCd-input' size='small' color='primary' /> diff --git a/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx b/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx index 2537d1ea4..caf382f52 100644 --- a/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx +++ b/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx @@ -12,12 +12,17 @@ import { MetadataType } from '../../../../../store/metadata'; import ReferenceModels from './ReferenceModels'; import SceneDataForm from './SceneDataForm'; import { apolloClient } from '../../../../../graphql/index'; -import { GetSceneForAssetVersionDocument, RelatedObjectType, useGetSubjectQuery } from '../../../../../types/graphql'; +import { GetSceneForAssetVersionDocument, RelatedObjectType, useGetSubjectQuery, GetIngestTitleDocument, GetIngestTitleQuery } from '../../../../../types/graphql'; import { eSystemObjectType } from '@dpo-packrat/common'; import { toast } from 'react-toastify'; import RelatedObjectsList from '../Model/RelatedObjectsList'; import ObjectSelectModal from '../Model/ObjectSelectModal'; import { TextArea } from '../../../../../components'; +// import clsx from 'clsx'; +import lodash from 'lodash'; +import SubtitleControl from '../Control/SubtitleControl'; +import { ApolloQueryResult } from '@apollo/client'; +import { parseSubtitlesToState } from '../../../../../store/utils'; const useStyles = makeStyles(() => ({ container: { @@ -147,10 +152,28 @@ function Scene(props: SceneProps): React.ReactElement { await setModalOpen(true); }; - const onRemoveSourceObject = (idSystemObject: number): void => { + const onRemoveSourceObject = async (idSystemObject: number): Promise => { const { sourceObjects } = scene; const updatedSourceObjects = sourceObjects.filter(sourceObject => sourceObject.idSystemObject !== idSystemObject); updateMetadataField(metadataIndex, 'sourceObjects', updatedSourceObjects, MetadataType.scene); + + const { data: { getIngestTitle: { ingestTitle }}}: ApolloQueryResult = await apolloClient.query({ + query: GetIngestTitleDocument, + variables: { + input: { + sourceObjects: scene.sourceObjects + } + }, + fetchPolicy: 'no-cache' + }); + + if (!ingestTitle) { + toast.error('Failed to fetch titles for ingestion items'); + return + } + const subtitleState = parseSubtitlesToState(ingestTitle); + updateMetadataField(metadataIndex, 'subtitles', subtitleState, MetadataType.scene); + updateMetadataField(metadataIndex, 'name', ingestTitle.title, MetadataType.scene); }; const onRemoveDerivedObject = (idSystemObject: number): void => { @@ -166,11 +189,57 @@ function Scene(props: SceneProps): React.ReactElement { resetRepositoryBrowserRoot(); }; - const onSelectedObjects = (newSourceObjects: StateRelatedObject[]) => { + const onSelectedObjects = async (newSourceObjects: StateRelatedObject[]) => { updateMetadataField(metadataIndex, objectRelationship === RelatedObjectType.Source ? 'sourceObjects' : 'derivedObjects', newSourceObjects, MetadataType.scene); + + if (objectRelationship === RelatedObjectType.Source) { + const { data: { getIngestTitle: { ingestTitle }}}: ApolloQueryResult = await apolloClient.query({ + query: GetIngestTitleDocument, + variables: { + input: { + sourceObjects: scene.sourceObjects + } + }, + fetchPolicy: 'no-cache' + }); + + if (!ingestTitle) { + toast.error('Failed to fetch titles for ingestion items'); + return + } + const subtitleState = parseSubtitlesToState(ingestTitle); + updateMetadataField(metadataIndex, 'subtitles', subtitleState, MetadataType.scene); + updateMetadataField(metadataIndex, 'name', ingestTitle.title, MetadataType.scene); + } + onModalClose(); }; + const onSelectSubtitle = (id: number) => { + const updatedSubtitles = scene.subtitles.map((subtitle) => { + return { + id: subtitle.id, + value: subtitle.value, + subtitleOption: subtitle.subtitleOption, + selected: id === subtitle.id + } + }); + updateMetadataField(metadataIndex, 'subtitles', updatedSubtitles, MetadataType.scene); + }; + + const onUpdateCustomSubtitle = (event: React.ChangeEvent, id: number) => { + const subtitlesCopy = lodash.cloneDeep(scene.subtitles); + const targetSubtitle = subtitlesCopy.find(subtitle => subtitle.id === id); + + if (!targetSubtitle) { + toast.warn('Something went wrong with updating the subtitle. Please try again'); + return; + } + + targetSubtitle.value = event.target.value; + updateMetadataField(metadataIndex, 'subtitles', subtitlesCopy, MetadataType.scene); + }; + return ( {idAsset && ( @@ -215,13 +284,20 @@ function Scene(props: SceneProps): React.ReactElement { /> + + + )} (false); const [breadcrumbNames, setBreadcrumbNames] = useState([]); - const getSelectedProject = useProjectStore(state => state.getSelectedProject); const getSelectedItem = useItemStore(state => state.getSelectedItem); const [metadatas, getMetadataInfo, validateFields] = useMetadataStore(state => [state.metadatas, state.getMetadataInfo, state.validateFields]); const { ingestionStart, ingestionComplete } = useIngest(); @@ -115,8 +112,8 @@ function Metadata(): React.ReactElement { return ; } - const project = getSelectedProject(); const item = getSelectedItem(); + const project = item?.projectName; const assetType = getAssetType(Number.parseInt(type, 10)); const onPrevious = async () => { @@ -175,7 +172,6 @@ function Metadata(): React.ReactElement { } = nextMetadata; const { isLast } = getMetadataInfo(id); const nextRoute = resolveSubRoute(HOME_ROUTES.INGESTION, `${INGESTION_ROUTE.ROUTES.METADATA}?fileId=${id}&type=${type}&last=${isLast}`); - // console.log(`Metadata onNext() nextRoute=${nextRoute}, assetType=${JSON.stringify(assetType)}, metadataIndex=${metadataIndex}, metadatas=${JSON.stringify(metadatas)}`); history.push(nextRoute); } }; @@ -203,9 +199,9 @@ function Metadata(): React.ReactElement { const calculateBreadcrumbPath = (): React.ReactNode => { if (breadcrumbNames) { - return ; + return ; } else { - return ; + return ; } }; @@ -232,7 +228,7 @@ function Metadata(): React.ReactElement { } interface BreadcrumbsHeaderProps { - project: StateProject | undefined; + projectName: string | undefined; item: StateItem | undefined; metadata: StateMetadata; customBreadcrumbs?: boolean; @@ -241,7 +237,7 @@ interface BreadcrumbsHeaderProps { function BreadcrumbsHeader(props: BreadcrumbsHeaderProps) { const classes = useStyles(); - const { project, item, metadata, customBreadcrumbs, customBreadcrumbsArr } = props; + const { projectName, item, metadata, customBreadcrumbs, customBreadcrumbsArr } = props; let content: React.ReactNode; @@ -259,8 +255,8 @@ function BreadcrumbsHeader(props: BreadcrumbsHeaderProps) { } else { content = ( }> - Specify metadata for: {project?.name} - {item?.name} + Specify metadata for: Project: {projectName} + Media Group: {item?.subtitle} {metadata.file.name} ); diff --git a/client/src/pages/Ingestion/components/SubjectItem/ItemList.tsx b/client/src/pages/Ingestion/components/SubjectItem/ItemList.tsx index 3d50491f3..7d4bb08b6 100644 --- a/client/src/pages/Ingestion/components/SubjectItem/ItemList.tsx +++ b/client/src/pages/Ingestion/components/SubjectItem/ItemList.tsx @@ -3,15 +3,16 @@ * * This component renders item list used in SubjectItem component. */ -import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@material-ui/core'; +import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Select, MenuItem, Typography } from '@material-ui/core'; import { grey } from '@material-ui/core/colors'; import { makeStyles } from '@material-ui/core/styles'; import React from 'react'; import { DebounceInput } from 'react-debounce-input'; -import { MdCheckBox, MdCheckBoxOutlineBlank } from 'react-icons/md'; import { RiCheckboxBlankCircleLine, RiRecordCircleFill } from 'react-icons/ri'; -import { defaultItem, StateItem, useItemStore } from '../../../../store'; +import { BsPlusCircle } from 'react-icons/bs'; +import { StateItem, useItemStore, useSubjectStore } from '../../../../store'; import { palette } from '../../../../theme'; +import lodash from 'lodash'; const useStyles = makeStyles(({ palette, spacing, typography, breakpoints }) => ({ container: { @@ -47,7 +48,7 @@ const useStyles = makeStyles(({ palette, spacing, typography, breakpoints }) => border: 'none', outline: 'none', padding: '0px 2px', - fontSize: '1em', + fontSize: '0.75rem', fontWeight: typography.fontWeightRegular, fontFamily: typography.fontFamily, '&:focus': { @@ -59,82 +60,60 @@ const useStyles = makeStyles(({ palette, spacing, typography, breakpoints }) => '&::-moz-placeholder': { fontStyle: 'italic' } + }, + projectSelect: { + width: '100%', + height: 'fit-content', + backgroundColor: palette.background.paper, + fontSize: '0.75rem' + }, + text: { + fontSize: '0.75rem', + fontWeight: typography.fontWeightRegular, + fontFamily: typography.fontFamily } })); function ItemList(): React.ReactElement { const classes = useStyles(); - const [items, updateItem] = useItemStore(state => [state.items, state.updateItem]); - + const [items, hasNewItem, newItem, addNewItem, projectList, updateNewItemSubtitle, updateNewItemEntireSubject, updateNewItemProject, updateSelectedItem] = useItemStore(state => [state.items, /*state.updateItem,*/ state.hasNewItem, state.newItem, state.addNewItem, state.projectList, state.updateNewItemSubtitle, state.updateNewItemEntireSubject, state.updateNewItemProject, state.updateSelectedItem]); + const [subjects] = useSubjectStore(state => [state.subjects]); const selectableHeaderStyle = { width: 100 }; const getItemsList = (item: StateItem, index: number) => { - const { id, selected, name, entireSubject } = item; - const isDefaultItem = id === defaultItem.id; - - let content: React.ReactNode = ( - - {name} - - ); - - const onUpdateSelected = (selected: boolean) => { - updateItem({ ...item, selected }); - }; - - const onUpdateEntireSubject = (entireSubject: boolean) => { - updateItem({ ...item, entireSubject }); - }; - - const onUpdateName = (event: React.ChangeEvent) => { - const { target } = event; - const name = target.value; - - updateItem({ ...item, name }); - }; - - if (isDefaultItem) { - content = ( - - ); - } + const { id, selected, subtitle, entireSubject, projectName } = item; return ( - - {content} - - + entireSubject={entireSubject as boolean} + projectName={projectName} + onUpdateSelected={updateSelectedItem} + id={id} + /> ); }; + return ( -
+
Selected - Name + Project Full Subject? + Subtitle + - {items.map(getItemsList)} + {(items && items.length > 0) && items.map(getItemsList)} + {hasNewItem ? 1} /> : }
@@ -142,17 +121,49 @@ function ItemList(): React.ReactElement { } interface ItemListItemProps { - isDefaultItem: boolean; selected: boolean; entireSubject: boolean; - onUpdateSelected: (selected: boolean) => void; - onUpdateEntireSubject: (entireSubject: boolean) => void; + subtitle: string; children?: React.ReactNode; + projectName: string; + id: string; + onUpdateSelected: (id: string) => void; } function ItemListItem(props: ItemListItemProps) { const classes = useStyles(); - const { isDefaultItem, selected, onUpdateSelected, onUpdateEntireSubject, entireSubject, children } = props; + const { selected, subtitle, entireSubject, projectName, onUpdateSelected, id } = props; + + const cellStyle = { + width: 100, + }; + + return ( + + + {!selected && onUpdateSelected(id)} size={20} color={grey[400]} />} + {selected && onUpdateSelected(id)} size={20} color={palette.primary.main} />} + + + {projectName} + + + {entireSubject ? 'Yes' : 'No'} + + + {subtitle.length ? subtitle : 'None'} + + + ); +} + +interface ItemListEmptyItemProps { + onAddItem: () => void; +} + +function ItemListEmptyItem(props: ItemListEmptyItemProps) { + const classes = useStyles(); + const { onAddItem } = props; const cellStyle = { width: 100, @@ -161,17 +172,80 @@ function ItemListItem(props: ItemListItemProps) { return ( - {!selected && onUpdateSelected(true)} size={20} color={grey[400]} />} - {selected && onUpdateSelected(false)} size={20} color={palette.primary.main} />} + - {children} + + Add new media group here + + + + + ); +} + +interface ItemListNewItemProps { + item: StateItem; + onUpdateSelected: (id: string) => void; + onUpdateEntireSubject: (entire: boolean) => void; + onUpdateName: (event: React.ChangeEvent) => void; + onUpdateProject: (idProject: number) => void; + hasMultipleSubjects: boolean; + projects: any[]; +} + +function ItemListNewItem(props: ItemListNewItemProps) { + const { item: { subtitle, entireSubject, idProject, projectName, id, selected }, onUpdateEntireSubject, onUpdateName, onUpdateProject, onUpdateSelected, projects, hasMultipleSubjects } = props; + const classes = useStyles(); + + const uniqueSortedProjects = lodash.uniqBy(lodash.orderBy(projects, 'Name', 'asc'), 'Name'); + + const cellStyle = { + width: 100, + }; + + return ( + - {isDefaultItem ? ( - <> - {!entireSubject && onUpdateEntireSubject(true)} size={20} color={grey[500]} />} - {entireSubject && onUpdateEntireSubject(false)} size={20} color={palette.primary.main} />} - - ) : entireSubject ? 'Yes' : 'No'} + {!selected && onUpdateSelected(id)} size={20} color={grey[400]} />} + {selected && onUpdateSelected(id)} size={20} color={palette.primary.main} />} + + + + + + {hasMultipleSubjects ? No : } + + + + {(idProject > -1) && ()} + ); diff --git a/client/src/pages/Ingestion/components/SubjectItem/ProjectList.tsx b/client/src/pages/Ingestion/components/SubjectItem/ProjectList.tsx deleted file mode 100644 index b1a03a04a..000000000 --- a/client/src/pages/Ingestion/components/SubjectItem/ProjectList.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/** - * ProjectList - * - * This component renders project list used in SubjectItem component. - */ -import { MenuItem, Select } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import lodash from 'lodash'; -import React from 'react'; -import { useProjectStore } from '../../../../store'; - -const useStyles = makeStyles(({ palette }) => ({ - projectSelect: { - width: '100%', - backgroundColor: palette.background.paper, - fontSize: '0.8em' - } -})); - -function ProjectList(): React.ReactElement { - const classes = useStyles(); - const [projects, getSelectedProject, updateSelectedProject] = useProjectStore(state => [state.projects, state.getSelectedProject, state.updateSelectedProject]); - - const noProjects = !projects.length; - const selectedProject = getSelectedProject(); - - const uniqueSortedProjects = lodash.uniqBy(lodash.orderBy(projects, 'name', 'asc'), 'name'); - - return ( - - ); -} - -export default ProjectList; \ No newline at end of file diff --git a/client/src/pages/Ingestion/components/SubjectItem/index.tsx b/client/src/pages/Ingestion/components/SubjectItem/index.tsx index 8dcdae6f7..b81e63849 100644 --- a/client/src/pages/Ingestion/components/SubjectItem/index.tsx +++ b/client/src/pages/Ingestion/components/SubjectItem/index.tsx @@ -10,9 +10,8 @@ import { Redirect, useHistory } from 'react-router'; import { toast } from 'react-toastify'; import { FieldType, SidebarBottomNavigator } from '../../../../components'; import { HOME_ROUTES, INGESTION_ROUTE, resolveSubRoute } from '../../../../constants'; -import { useItemStore, useMetadataStore, useProjectStore, useSubjectStore, useVocabularyStore } from '../../../../store'; +import { useItemStore, useMetadataStore, useSubjectStore, useVocabularyStore } from '../../../../store'; import ItemList from './ItemList'; -import ProjectList from './ProjectList'; import SearchList from './SearchList'; import SubjectList from './SubjectList'; import { Helmet } from 'react-helmet'; @@ -48,15 +47,13 @@ function SubjectItem(): React.ReactElement { const history = useHistory(); const [subjectError, setSubjectError] = useState(false); - const [projectError, setProjectError] = useState(false); const [itemError, setItemError] = useState(false); const [metadataStepLoading, setMetadataStepLoading] = useState(false); const updateVocabularyEntries = useVocabularyStore(state => state.updateVocabularyEntries); const subjects = useSubjectStore(state => state.subjects); - const [projects, projectsLoading, getSelectedProject] = useProjectStore(state => [state.projects, state.loading, state.getSelectedProject]); const [itemsLoading, getSelectedItem] = useItemStore(state => [state.loading, state.getSelectedItem]); - const [metadatas, updateMetadataFolders, getMetadataInfo] = useMetadataStore(state => [state.metadatas, state.updateMetadataFolders, state.getMetadataInfo]); + const [metadatas, updateMetadataFolders, getMetadataInfo, initializeSubtitlesForModels] = useMetadataStore(state => [state.metadatas, state.updateMetadataFolders, state.getMetadataInfo, state.initializeSubtitlesForModels]); const { ingestionReset } = useIngest(); const selectedItem = getSelectedItem(); @@ -66,22 +63,19 @@ function SubjectItem(): React.ReactElement { } }, [subjects]); - useEffect(() => { - if (projects.length > 0) { - setProjectError(false); - } - }, [projects]); - useEffect(() => { if (selectedItem) { - if (selectedItem.name.length) { + if (selectedItem.subtitle.length) { setItemError(false); } } }, [selectedItem]); const onNext = async (): Promise => { + toast.dismiss(); let error: boolean = false; + // Note: we only want certain warnings to flag if we have missing fields after selecting an new item + let isItemSelected = !!selectedItem; if (!subjects.length) { error = true; @@ -89,28 +83,33 @@ function SubjectItem(): React.ReactElement { toast.warn('Please provide at least one subject', { autoClose: false }); } - const selectedProject = getSelectedProject(); + if (!selectedItem) { + error = true; + setItemError(true); + toast.warn('Please select or provide a media group', { autoClose: false }); + } - if (!selectedProject) { + if (isItemSelected && selectedItem?.idProject === -1) { error = true; - setProjectError(true); - toast.warn('Please select a project', { autoClose: false }); + setItemError(true); + toast.warn('Please select a project for media group', { autoClose: false }); } - if (!selectedItem) { + if (isItemSelected && selectedItem?.entireSubject === null) { error = true; setItemError(true); - toast.warn('Please select or provide a media group', { autoClose: false }); + toast.warn('Please indicate whether media group is entire subject', { autoClose: false }); } - if (selectedItem?.name.trim() === '') { + if (isItemSelected && (subjects.length > 1 || !selectedItem?.entireSubject) && selectedItem?.subtitle.trim() === '') { error = true; setItemError(true); - toast.warn('Please provide a valid name for media group', { autoClose: false }); + toast.warn('Please provide a valid subtitle for media group', { autoClose: false }); } if (error) return; + await initializeSubtitlesForModels(); try { setMetadataStepLoading(true); await updateVocabularyEntries(); @@ -126,7 +125,6 @@ function SubjectItem(): React.ReactElement { const { file: { id, type } } = metadatas[0]; const { isLast } = getMetadataInfo(id); const nextRoute = resolveSubRoute(HOME_ROUTES.INGESTION, `${INGESTION_ROUTE.ROUTES.METADATA}?fileId=${id}&type=${type}&last=${isLast}`); - // console.log(`SubjectItem onNext() nextRoute=${nextRoute}, metadatas=${JSON.stringify(metadatas)}`); toast.dismiss(); history.push(nextRoute); }; @@ -158,18 +156,6 @@ function SubjectItem(): React.ReactElement { - - - - [state.removeSelectedUploads, state.reset, state.getSelectedFiles, state.completed]); const [subjects, resetSubjects] = useSubjectStore(state => [state.subjects, state.reset]); - const [getSelectedProject, resetProjects] = useProjectStore(state => [state.getSelectedProject, state.reset]); const [getSelectedItem, resetItems] = useItemStore(state => [state.getSelectedItem, state.reset]); const [metadatas, getSelectedIdentifiers, resetMetadatas, getMetadatas] = useMetadataStore(state => [state.metadatas, state.getSelectedIdentifiers, state.reset, state.getMetadatas]); const getAssetType = useVocabularyStore(state => state.getAssetType); @@ -80,14 +77,7 @@ function useIngest(): UseIngest { unit })); - const project: StateProject = getSelectedProject() || { id: 0, name: '', selected: false }; - - const ingestProject: IngestProjectInput = { - id: project.id, - name: project.name - }; - - const item: StateItem = getSelectedItem() || { id: '', entireSubject: false, selected: false, name: '' }; + const item: StateItem = getSelectedItem() || { id: '', entireSubject: false, selected: false, subtitle: '', idProject: -1, projectName: '' }; const isDefaultItem = item.id === defaultItem.id; @@ -99,8 +89,13 @@ function useIngest(): UseIngest { const ingestItem: IngestItemInput = { id: ingestItemId, - subtitle: '', // FIXME -- make sure to pass just the subtitle here! Previously, was item.name - entireSubject: item.entireSubject + subtitle: item.subtitle, + entireSubject: item.entireSubject as boolean + }; + + const ingestProject: IngestProjectInput = { + id: item.idProject, + name: item.projectName }; const ingestPhotogrammetry: IngestPhotogrammetryInput[] = []; @@ -189,7 +184,8 @@ function useIngest(): UseIngest { directory, sourceObjects, derivedObjects, - updateNotes + updateNotes, + subtitles } = model; let { @@ -203,9 +199,9 @@ function useIngest(): UseIngest { } const ingestIdentifiers: IngestIdentifierInput[] = getIngestIdentifiers(identifiers); - + const selectedSubtitle = subtitles.find(subtitle => subtitle.selected); const modelData: IngestModelInput = { - subtitle: '', // FIXME -- make sure to pass just the subtitle here! Previously, was model.name + subtitle: selectedSubtitle?.value as string, idAssetVersion: parseFileId(file.id), dateCreated, identifiers: ingestIdentifiers, @@ -232,14 +228,14 @@ function useIngest(): UseIngest { if (isScene) { const { identifiers, systemCreated, approvedForPublication, posedAndQCd, directory, sourceObjects, - derivedObjects, updateNotes } = scene; + derivedObjects, updateNotes, subtitles } = scene; const ingestIdentifiers: IngestIdentifierInput[] = getIngestIdentifiers(identifiers); - + const selectedSubtitle = subtitles.find(subtitle => subtitle.selected); const sceneData: IngestSceneInput = { idAssetVersion: parseFileId(file.id), identifiers: ingestIdentifiers, systemCreated, - subtitle: '', // FIXME -- make sure to pass just the subtitle here! Previously, was scene.name + subtitle: selectedSubtitle?.value as string, approvedForPublication, posedAndQCd, directory, @@ -335,7 +331,6 @@ function useIngest(): UseIngest { const resetIngestionState = () => { resetSubjects(); - resetProjects(); resetItems(); resetMetadatas(); }; diff --git a/client/src/pages/Repository/components/DetailsView/DetailsTab/ItemDetails.tsx b/client/src/pages/Repository/components/DetailsView/DetailsTab/ItemDetails.tsx index a4417e5e2..ff0439841 100644 --- a/client/src/pages/Repository/components/DetailsView/DetailsTab/ItemDetails.tsx +++ b/client/src/pages/Repository/components/DetailsView/DetailsTab/ItemDetails.tsx @@ -6,9 +6,8 @@ */ import { Box } from '@material-ui/core'; import React, { useEffect } from 'react'; -import { /*CheckboxField,*/ Loader } from '../../../../../components'; +import { Loader } from '../../../../../components'; import { SubjectDetailFields } from '../../../../../types/graphql'; -// import { isFieldUpdated } from '../../../../../utils/repository'; import { DetailComponentProps } from './index'; import { SubjectFields } from './SubjectDetails'; import { eSystemObjectType } from '@dpo-packrat/common'; diff --git a/client/src/pages/Repository/components/DetailsView/DetailsTab/SubjectDetails.tsx b/client/src/pages/Repository/components/DetailsView/DetailsTab/SubjectDetails.tsx index 4e91ebd87..f421b5a28 100644 --- a/client/src/pages/Repository/components/DetailsView/DetailsTab/SubjectDetails.tsx +++ b/client/src/pages/Repository/components/DetailsView/DetailsTab/SubjectDetails.tsx @@ -49,6 +49,7 @@ interface SubjectFieldsProps extends SubjectDetailFields { onChange: (event: React.ChangeEvent) => void; isItemView?: boolean; setCheckboxField?: (event) => void; + // TOFIX? ItemDetails?: any; itemData?: any; } @@ -76,26 +77,52 @@ export function SubjectFields(props: SubjectFieldsProps): React.ReactElement { - {(isItemView && setCheckboxField && ItemDetails && itemData) && - - - Entire Subject - - - - - - } + {(isItemView && setCheckboxField && ItemDetails && itemData) && ( + + + + Subtitle + + + + + + + + Entire Subject + + + + + + + )} { isItemView ? null : ( <> diff --git a/client/src/pages/Repository/components/DetailsView/index.tsx b/client/src/pages/Repository/components/DetailsView/index.tsx index 15df1381c..9f2a222bb 100644 --- a/client/src/pages/Repository/components/DetailsView/index.tsx +++ b/client/src/pages/Repository/components/DetailsView/index.tsx @@ -472,12 +472,14 @@ function DetailsView(): React.ReactElement { } }; + const immutableNameTypes = new Set([eSystemObjectType.eItem, eSystemObjectType.eModel, eSystemObjectType.eScene]); + return ( StateItem | undefined; addItems: (items: StateItem[]) => void; - updateItem: (item: StateItem) => void; + addNewItem: () => Promise; + updateNewItemEntireSubject: (entireSubject: boolean) => void; + updateNewItemSubtitle: (event: React.ChangeEvent) => void; + updateNewItemProject: (idProject: number) => void; + updateSelectedItem: (id: string) => void; loadingItems: () => void; + fetchIngestionItems: (idSubjects: number[]) => Promise; reset: () => void; }; export const useItemStore = create((set: SetState, get: GetState) => ({ - items: [defaultItem], + items: [], + newItem: { ...defaultItem }, + hasNewItem: false, loading: false, + projectList: [], getSelectedItem: (): StateItem | undefined => { - const { items } = get(); - return lodash.find(items, { selected: true }); + const { items, newItem } = get(); + const selectedItem = newItem.selected ? newItem : lodash.find(items, { selected: true }); + return selectedItem; }, addItems: (fetchedItems: StateItem[]): void => { const { items } = get(); const currentDefaultItem = lodash.find(items, { id: defaultItem.id }); - if (currentDefaultItem) { - if (!fetchedItems.length) { - const selectedDefaultItem = { ...currentDefaultItem, selected: true }; - set({ items: [selectedDefaultItem], loading: false }); + if (!fetchedItems.length) return; - } - - currentDefaultItem.selected = false; + const newItemSelected = lodash.find(fetchedItems, { selected: true }); if (!newItemSelected) fetchedItems[0].selected = true; - const newItems: StateItem[] = fetchedItems.concat([currentDefaultItem]); + if (currentDefaultItem) + currentDefaultItem.selected = false; + + const newItems: StateItem[] = [...fetchedItems]; + if (currentDefaultItem) + newItems.push(currentDefaultItem); + set({ items: newItems, loading: false }); + }, + addNewItem: async () => { + const { items, newItem } = get(); + try { + const projectListQuery: ApolloQueryResult = await apolloClient.query({ + query: GetProjectListDocument, + variables: { + input: { + search: '' + } + }, + fetchPolicy: 'no-cache' + }); + const { data: { getProjectList: { projects } } } = projectListQuery; + if (projects) + set({ projectList: projects as Project[] }); + } catch (error) { + toast.error('Failed to get media group for subjects'); } + const itemsCopy = lodash.cloneDeep(items); + if (itemsCopy && itemsCopy.length) + itemsCopy.forEach(item => item.selected = false); + const newItemCopy = lodash.cloneDeep(newItem); + newItemCopy.selected = true; + set({ hasNewItem: true, items: itemsCopy, newItem: newItemCopy }); }, - updateItem: (item: StateItem): void => { - const { items, getSelectedItem } = get(); - const { selected } = item; - - let updatedItems = items; + + updateNewItemEntireSubject: (entireSubject: boolean) => { + const { newItem } = get(); + const newItemCopy = lodash.cloneDeep(newItem); + set({ newItem: { ...newItemCopy, entireSubject }}); + }, + updateNewItemSubtitle: (event: React.ChangeEvent) => { + const { newItem } = get(); - const mapItems = (newItem: StateItem): StateItem[] => - lodash.map(updatedItems, item => { - if (item.id === newItem.id) { - return newItem; - } - return item; - }); + const { target: { value: subtitle } } = event; + set({ newItem: { ...newItem, subtitle }}) + }, + updateNewItemProject: (idProject: number) => { + const { newItem, projectList } = get(); + const newItemCopy = lodash.cloneDeep(newItem); + const newProjectName = projectList.find(project => project.idProject === idProject)?.Name ?? ''; + newItemCopy.idProject = idProject; + newItemCopy.projectName = newProjectName; + set({ newItem: newItemCopy }); + }, + updateSelectedItem: (id: string): void => { + const { items, newItem } = get(); + if (id === newItem.id) { + newItem.selected = !newItem.selected; + if (items && items.length) + items.forEach(item => item.selected = false); - if (selected) { - const alreadySelected: StateItem | undefined = getSelectedItem(); - if (alreadySelected) { - const unselectedItem = { - ...alreadySelected, - selected: false - }; - updatedItems = mapItems(unselectedItem); - set({ items: updatedItems }); - } + set({ items, newItem }); + return; } - - updatedItems = mapItems(item); - set({ items: updatedItems }); + newItem.selected = false; + const updatedItems = items.map((item) => { + return { + ...item, + selected: id === item.id ? !item.selected : false + } + }) + set({ newItem, items: updatedItems }) }, loadingItems: (): void => { set({ loading: true }); }, + fetchIngestionItems: async (idSubjects: number[]): Promise => { + try { + const ingestionItemQuery: ApolloQueryResult = await apolloClient.query({ + query: GetIngestionItemsDocument, + variables: { + input: { + idSubjects + } + }, + fetchPolicy: 'no-cache' + }); + const { data: { getIngestionItems: { IngestionItem }} } = ingestionItemQuery; + const ingestionItemState = IngestionItem?.map(item => parseIngestionItemToState(item)); + set({ items: ingestionItemState }); + } catch (error) { + toast.error('Failed to get media group for subjects'); + } + }, reset: (): void => { - set({ items: [defaultItem], loading: false }); + set({ items: [], loading: false, hasNewItem: false, newItem: { ...defaultItem } }); } })); diff --git a/client/src/store/metadata/index.ts b/client/src/store/metadata/index.ts index 0410c0823..86444d6e9 100644 --- a/client/src/store/metadata/index.ts +++ b/client/src/store/metadata/index.ts @@ -16,14 +16,15 @@ import { GetAssetVersionsDetailsDocument, GetAssetVersionsDetailsQuery, GetContentsForAssetVersionsDocument, - Project + Project, + GetIngestTitleDocument, + GetIngestTitleQuery } from '../../types/graphql'; import { eVocabularySetID } from '@dpo-packrat/common'; -import { StateItem, useItemStore } from '../item'; -import { StateProject, useProjectStore } from '../project'; +import { StateItem, useItemStore, StateProject } from '../item'; import { StateSubject, useSubjectStore } from '../subject'; import { FileId, IngestionFile, useUploadStore } from '../upload'; -import { parseFileId, parseFoldersToState, parseIdentifiersToState, parseItemToState, parseProjectToState, parseSubjectUnitIdentifierToState } from '../utils'; +import { parseFileId, parseFoldersToState, parseIdentifiersToState, parseItemToState, parseProjectToState, parseSubjectUnitIdentifierToState, parseSubtitlesToState } from '../utils'; import { useVocabularyStore } from '../vocabulary'; import { defaultModelFields, defaultOtherFields, defaultPhotogrammetryFields, defaultSceneFields, ValidateFieldsSchema, defaultSceneAttachmentFields } from './metadata.defaults'; import { @@ -40,7 +41,7 @@ import { StateFolder, StateIdentifier, StateMetadata, - ValidateFields + ValidateFields, } from './metadata.types'; type MetadataStore = { @@ -54,6 +55,7 @@ type MetadataStore = { updateMetadataSteps: (existingMetadata: any) => Promise; updateMetadataField: (metadataIndex: Readonly, name: string, value: MetadataFieldValue, metadataType: MetadataType) => void; updateMetadataFolders: () => Promise; + initializeSubtitlesForModels: () => Promise; updateCameraSettings: (metadatas: StateMetadata[]) => Promise; reset: () => void; getMetadatas: () => StateMetadata[]; @@ -145,7 +147,6 @@ export const useMetadataStore = create((set: SetState() }); const selectedFiles = getSelectedFiles(completed, true); @@ -208,7 +209,6 @@ export const useMetadataStore = create((set: SetState((set: SetState((set: SetState asset.idAssetVersion === idAssetVersion && asset?.Model)?.Model; const updateScene = UpdatedAssetVersionMetadata.find((asset) => asset.idAssetVersion === idAssetVersion && asset?.Scene)?.Scene; const updatePhoto = UpdatedAssetVersionMetadata.find((asset) => asset.idAssetVersion === idAssetVersion && asset?.CaptureDataPhoto)?.CaptureDataPhoto; - // console.log(`useMetaStore idAssetVersion=${idAssetVersion}; existingIdAssetVersion=${existingIdAssetVersion}`); - // console.log(`useMetaStore updateModel=${JSON.stringify(updateModel)}`); - // console.log(`useMetaStore updateScene=${JSON.stringify(updateScene)}`); - // console.log(`useMetaStore updatePhoto=${JSON.stringify(updatePhoto)}`); if (foundSubjectUnitIdentifier) { const subject: StateSubject = parseSubjectUnitIdentifierToState(foundSubjectUnitIdentifier); @@ -247,6 +242,7 @@ export const useMetadataStore = create((set: SetState((set: SetState((set: SetState((set: SetState => { + const { metadatas } = get(); + const { getSelectedItem } = useItemStore.getState(); + const selectedItem = getSelectedItem(); + + try { + const { data: { getIngestTitle: { ingestTitle }}}: ApolloQueryResult = await apolloClient.query({ + query: GetIngestTitleDocument, + variables: { + input: { + item: { + id: Number(selectedItem?.id), + subtitle: selectedItem?.subtitle, + entireSubject: selectedItem?.entireSubject + } + } + } + }); + + if (!ingestTitle) { + toast.error('Failed to fetch titles for ingestion items'); + return + } + // console.log('ingestTitle', ingestTitle); + const metadatasCopy = lodash.cloneDeep(metadatas); + const subtitleState = parseSubtitlesToState(ingestTitle); + + metadatasCopy.forEach(metadata => { + if (metadata.model) { + metadata.model.subtitles = subtitleState; + metadata.model.name = ingestTitle.title; + } + }) + + // console.log('metadatasCopy', metadatasCopy); + set({ metadatas: metadatasCopy }); + } catch (error) { + toast.error(`Failed to fetch titles for ingestion items ${error}`); + } + + }, getInitialStateFolders: (folders: string[]): StateFolder[] => { const { getInitialEntry, getEntries } = useVocabularyStore.getState(); const stateFolders: StateFolder[] = folders.map((folder, index: number) => { diff --git a/client/src/store/metadata/metadata.defaults.ts b/client/src/store/metadata/metadata.defaults.ts index f7a030931..0d1765880 100644 --- a/client/src/store/metadata/metadata.defaults.ts +++ b/client/src/store/metadata/metadata.defaults.ts @@ -4,7 +4,10 @@ * Default field definitions for the metadata store. */ import * as yup from 'yup'; -import { ModelFields, OtherFields, PhotogrammetryFields, SceneFields, SceneAttachmentFields } from './metadata.types'; +import { ModelFields, OtherFields, PhotogrammetryFields, SceneFields, SceneAttachmentFields, eSubtitleOption } from './metadata.types'; +import { eSystemObjectType } from '@dpo-packrat/common'; + +const MAX_INTEGER = 2147483647; const identifierWhenSelectedValidation = { is: true, @@ -24,6 +27,13 @@ const folderSchema = yup.object().shape({ variantType: yup.number().nullable(true) }); +const subtitleSchema = yup.object().shape({ + value: yup.string(), + selected: yup.boolean().required(), + subtitleOption: yup.number().required(), + id: yup.number() +}) + const identifierValidation = { test: array => array.length && array.every(identifier => identifier.identifier.length), message: 'Should provide at least 1 identifier with valid identifier ID' @@ -34,11 +44,27 @@ const identifiersWhenValidation = { then: yup.array().of(identifierSchema).test(identifierValidation) }; +const hasModelSourcesValidation = { + test: array => array.length && array.some(source => source.objectType === eSystemObjectType.eModel), + message: 'Should provide at least 1 model parent for scene ingestion' +} + const notesWhenUpdate = { is: value => value > 0, then: yup.string().required() }; +const selectedSubtitleValidation = { + test: array => { + const selectedSubtitle = array.find(subtitle => subtitle.selected); + if (selectedSubtitle.subtitleOption !== eSubtitleOption.eNone) + return !!selectedSubtitle.value + + return true; + }, + message: 'Should provide a valid subtitle/name for ingestion' +}; + export const defaultPhotogrammetryFields: PhotogrammetryFields = { systemCreated: true, identifiers: [], @@ -76,20 +102,20 @@ export const photogrammetryFieldsSchemaUpdate = yup.object().shape({ .nullable(true) .typeError('Dataset Field ID must be a positive integer') .positive('Dataset Field ID must be a positive integer') - .max(2147483647, 'Dataset Field ID is too large'), + .max(MAX_INTEGER, 'Dataset Field ID is too large'), itemPositionType: yup.number().nullable(true), itemPositionFieldId: yup .number() .nullable(true) .typeError('Position Field ID must be a positive integer') .positive('Position Field ID must be a positive integer') - .max(2147483647, 'Position Field ID is too large'), + .max(MAX_INTEGER, 'Position Field ID is too large'), itemArrangementFieldId: yup .number() .nullable(true) .typeError('Arrangement Field ID must be a positive integer') .positive('Arrangement Field ID must be a positive integer') - .max(2147483647, 'Arrangement Field ID is too large'), + .max(MAX_INTEGER, 'Arrangement Field ID is too large'), focusType: yup.number().nullable(true), lightsourceType: yup.number().nullable(true), backgroundRemovalMethod: yup.number().nullable(true), @@ -99,7 +125,7 @@ export const photogrammetryFieldsSchemaUpdate = yup.object().shape({ .nullable(true) .typeError('Cluster Geometry Field ID must be a positive integer') .positive('Cluster Geometry Field ID must be a positive integer') - .max(2147483647, 'Cluster Geometry Field ID is too large'), + .max(MAX_INTEGER, 'Cluster Geometry Field ID is too large'), cameraSettingUniform: yup.boolean().required(), directory: yup.string(), updateNotes: yup.string().when('idAsset', notesWhenUpdate) @@ -136,7 +162,18 @@ export const defaultModelFields: ModelFields = { modelFileType: null, directory: '', updateNotes: '', - idAsset: 0 + idAsset: 0, + subtitles: [{ + value: '', + selected: true, + subtitleOption: eSubtitleOption.eInput, + id: 1 + }, { + value: '', + selected: false, + subtitleOption: eSubtitleOption.eNone, + id: 0 + }] }; export const modelFieldsSchemaUpdate = yup.object().shape({ @@ -173,6 +210,7 @@ export const modelFieldsSchemaUpdate = yup.object().shape({ export const modelFieldsSchema = modelFieldsSchemaUpdate.shape({ identifiers: yup.array().of(identifierSchema).when('systemCreated', identifiersWhenValidation), + subtitles: yup.array().of(subtitleSchema).test(selectedSubtitleValidation) }); export const defaultSceneFields: SceneFields = { @@ -188,7 +226,13 @@ export const defaultSceneFields: SceneFields = { posedAndQCd: false, canBeQCd: false, updateNotes: '', - idAsset: 0 + idAsset: 0, + subtitles: [{ + value: '', + selected: true, + subtitleOption: eSubtitleOption.eInput, + id: 0 + }] }; export const referenceModelSchema = yup.object().shape({ @@ -214,9 +258,10 @@ export const sceneFieldsSchemaUpdate = yup.object().shape({ export const sceneFieldsSchema = sceneFieldsSchemaUpdate.shape({ identifiers: yup.array().of(identifierSchema).when('systemCreated', identifiersWhenValidation), + sourceObjects: yup.array().of(sourceObjectSchema).test(hasModelSourcesValidation), + subtitles: yup.array().of(subtitleSchema).test(selectedSubtitleValidation) }); - export const defaultOtherFields: OtherFields = { systemCreated: true, identifiers: [], diff --git a/client/src/store/metadata/metadata.types.ts b/client/src/store/metadata/metadata.types.ts index 55afa75a4..3cf679d6f 100644 --- a/client/src/store/metadata/metadata.types.ts +++ b/client/src/store/metadata/metadata.types.ts @@ -23,6 +23,20 @@ export enum MetadataType { sceneAttachment = 'sceneAttachment' } +export enum eSubtitleOption { + eInherit, + eNone, + eInput, + eForced, +} + +export type SubtitleFields = { + value: string, + selected: boolean, + subtitleOption: eSubtitleOption, + id: number + }[] + export type MetadataInfo = { metadata: StateMetadata; readonly metadataIndex: number; @@ -45,7 +59,7 @@ export type FieldErrors = { }; }; -export type MetadataFieldValue = string | number | boolean | null | Date | StateIdentifier[] | StateFolder[] | StateRelatedObject[]; +export type MetadataFieldValue = string | number | boolean | null | Date | StateIdentifier[] | StateFolder[] | StateRelatedObject[] | SubtitleFields; export type MetadataUpdate = { valid: boolean; @@ -107,6 +121,7 @@ export type ModelFields = { directory: string; idAsset?: number; updateNotes?: string; + subtitles: SubtitleFields; }; export type SceneFields = { @@ -123,6 +138,7 @@ export type SceneFields = { canBeQCd: boolean; idAsset?: number; updateNotes?: string; + subtitles: SubtitleFields; }; export type OtherFields = { diff --git a/client/src/store/project.ts b/client/src/store/project.ts deleted file mode 100644 index 496e29a8e..000000000 --- a/client/src/store/project.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Project Store - * - * This store manages state for project used in Ingestion flow. - */ -import create, { SetState, GetState } from 'zustand'; -import lodash from 'lodash'; - -export type StateProject = { - id: number; - name: string; - selected: boolean; -}; - -type ProjectStore = { - projects: StateProject[]; - loading: boolean; - getSelectedProject: () => StateProject | undefined; - addProjects: (projects: StateProject[]) => void; - updateSelectedProject: (id: number) => void; - updateProject: (project: StateProject) => void; - loadingProjects: () => void; - reset: () => void; -}; - -export const useProjectStore = create((set: SetState, get: GetState) => ({ - projects: [], - loading: false, - getSelectedProject: (): StateProject | undefined => { - const { projects } = get(); - return lodash.find(projects, { selected: true }); - }, - addProjects: (fetchedProjects: StateProject[]) => { - if (!fetchedProjects.length) return; - set({ projects: fetchedProjects, loading: false }); - }, - updateSelectedProject: (id: number): void => { - const { projects, getSelectedProject, updateProject } = get(); - const project: StateProject | undefined = lodash.find(projects, { id }); - - if (project) { - const { selected } = project; - if (!selected) { - const alreadySelected: StateProject | undefined = getSelectedProject(); - - if (alreadySelected) { - const unselectedProject = { - ...alreadySelected, - selected: false - }; - - updateProject(unselectedProject); - } - project.selected = true; - } - - updateProject(project); - } - }, - updateProject: (project: StateProject): void => { - const { projects } = get(); - - const updatedProjects = (newProject: StateProject) => - lodash.map(projects, project => { - if (project.id === newProject.id) { - return newProject; - } - return project; - }); - - set({ projects: updatedProjects(project) }); - }, - loadingProjects: (): void => { - set({ loading: true }); - }, - reset: () => { - set({ projects: [], loading: false }); - } -})); diff --git a/client/src/store/repository.ts b/client/src/store/repository.ts index c06c42da2..c30382b2c 100644 --- a/client/src/store/repository.ts +++ b/client/src/store/repository.ts @@ -453,9 +453,10 @@ export const useRepositoryStore = create((set: SetState => { + console.log('idSystemObject', idSystemObject); const { getFilterState } = get(); const filter = getFilterState(); - const { data, error } = await getObjectChildrenForRoot(filter, idSystemObject); + const { data, error } = await getObjectChildrenForRoot(filter, 1); // set root to 0 for testing // const { data, error } = await getObjectChildrenForRoot(filter, 0); diff --git a/client/src/store/subject.ts b/client/src/store/subject.ts index e9f74a90c..d015c579e 100644 --- a/client/src/store/subject.ts +++ b/client/src/store/subject.ts @@ -5,22 +5,8 @@ */ import create, { SetState, GetState } from 'zustand'; import lodash from 'lodash'; -import { ApolloQueryResult } from '@apollo/client'; import { toast } from 'react-toastify'; -import { parseProjectToState, parseItemToState } from './utils'; -import { apolloClient } from '../graphql'; -import { - GetIngestionProjectsForSubjectsQuery, - GetIngestionProjectsForSubjectsDocument, - Project, - GetIngestionItemsForSubjectsQuery, - GetIngestionItemsForSubjectsDocument, - Item, - GetProjectListQuery, - GetProjectListDocument, -} from '../types/graphql'; -import { useItemStore, StateItem } from './item'; -import { useProjectStore, StateProject } from './project'; +import { useItemStore } from './item'; export type StateSubject = { id: number; @@ -42,14 +28,12 @@ type SubjectStore = { export const useSubjectStore = create((set: SetState, get: GetState) => ({ subjects: [], addSubjects: async (fetchedSubjects: StateSubject[]): Promise => { - // console.log(`subjectStore.addSubjects ${JSON.stringify(fetchedSubjects)}`); const { subjects, updateProjectsAndItemsForSubjects } = get(); const newSubjects: StateSubject[] = lodash.concat(subjects, fetchedSubjects); set({ subjects: newSubjects }); updateProjectsAndItemsForSubjects(newSubjects); }, addSubject: async (subject: StateSubject): Promise => { - // console.log(`subjectStore.addSubject ${JSON.stringify(subject)}`); const { subjects, addSubjects } = get(); const alreadyExists = subject.arkId ? !!lodash.find(subjects, { arkId: subject.arkId }) : false; @@ -68,85 +52,20 @@ export const useSubjectStore = create((set: SetState updateProjectsAndItemsForSubjects(selectedSubjects); }, updateProjectsAndItemsForSubjects: async (selectedSubjects: StateSubject[]): Promise => { - const { addProjects, loadingProjects } = useProjectStore.getState(); - const { addItems, loadingItems } = useItemStore.getState(); + const { addItems, fetchIngestionItems, updateNewItemEntireSubject } = useItemStore.getState(); if (!selectedSubjects.length) { addItems([]); - addProjects([]); return; } - const idSubjects = selectedSubjects.map(({ id }) => id); - - const variables = { - input: { - idSubjects - } - }; + if (selectedSubjects.length > 1) + updateNewItemEntireSubject(false); try { - loadingProjects(); - loadingItems(); - - // fetch list of all projects - const projectListQuery: ApolloQueryResult = await apolloClient.query({ - query: GetProjectListDocument, - variables: { - input: { - search: '' - } - }, - fetchPolicy: 'no-cache' - }); - const { data: { getProjectList: { projects: defaultProjectsList } } } = projectListQuery; - // console.log(`defaultProjectsList = ${JSON.stringify(defaultProjectsList)}`); - - // fetch list of projects associated with subject - const projectsQueryResult: ApolloQueryResult = await apolloClient.query({ - query: GetIngestionProjectsForSubjectsDocument, - variables, - fetchPolicy: 'no-cache' - }); - - // hash the associated projects and push the rest of projects - const projectQueryResultMap = new Map(); - const { data } = projectsQueryResult; - if (data) { - // console.log(`GetIngestionProjectsForSubjectsDocument = ${JSON.stringify(data)}`); - - const { Project: foundProjects, Default } = data.getIngestionProjectsForSubjects; - foundProjects.forEach((project) => projectQueryResultMap.set(project.idProject, project)); - - const projects: StateProject[] = foundProjects.map((project: Project, index: number) => parseProjectToState(project, Default ? false : !index)); - - for (let i = 0; i < defaultProjectsList.length; i++) { - if (projectQueryResultMap.has(defaultProjectsList[i].idProject)) continue; - - projects.push(parseProjectToState(defaultProjectsList[i] as Project, false)); - } - - addProjects(projects); - } - } catch (error) { - toast.error('Failed to get projects for subjects'); - } - - try { - const itemsQueryResult: ApolloQueryResult = await apolloClient.query({ - query: GetIngestionItemsForSubjectsDocument, - variables - }); - - const { data } = itemsQueryResult; - - if (data) { - const { Item: foundItems } = data.getIngestionItemsForSubjects; - const items: StateItem[] = foundItems.map((item: Item, index: number) => parseItemToState(item, false, index)); - addItems(items); - } + await fetchIngestionItems(selectedSubjects.map(subject => subject.id)); } catch (error) { - toast.error('Failed to get media group for subjects'); + toast.error('Failed to get ingestion items'); } }, reset: () => { diff --git a/client/src/store/utils.ts b/client/src/store/utils.ts index a5f911369..5b3a3f0e3 100644 --- a/client/src/store/utils.ts +++ b/client/src/store/utils.ts @@ -3,10 +3,9 @@ * * These are store specific utilities. */ -import { AssetVersion, IngestFolder, IngestIdentifier, Item, Project, SubjectUnitIdentifier, Vocabulary } from '../types/graphql'; -import { StateItem } from './item'; -import { StateFolder, StateIdentifier } from './metadata'; -import { StateProject } from './project'; +import { AssetVersion, IngestFolder, IngestIdentifier, Item, Project, SubjectUnitIdentifier, Vocabulary, IngestionItem, IngestTitle } from '../types/graphql'; +import { StateItem, StateProject } from './item'; +import { StateFolder, StateIdentifier, SubtitleFields, eSubtitleOption } from './metadata'; import { StateSubject } from './subject'; import { FileId, FileUploadStatus, IngestionFile } from './upload'; @@ -33,15 +32,31 @@ export function isNewItem(id: string): boolean { export function parseItemToState(item: Item, selected: boolean, position: number): StateItem { const { idItem, Name, EntireSubject } = item; const id = idItem || `${position}-new-item`; - + console.log('item', item); return { id: String(id), entireSubject: EntireSubject, - name: Name, - selected + subtitle: Name, + selected, + // TODO + idProject: 0, + projectName: '' }; } +export function parseIngestionItemToState(ingestionItem: IngestionItem): StateItem { + const { idItem, EntireSubject, MediaGroupName, idProject, ProjectName } = ingestionItem; + console.log('ingestionItem', ingestionItem); + return { + id: String(idItem), + subtitle: MediaGroupName, + entireSubject: EntireSubject, + selected: false, + idProject, + projectName: ProjectName + } +} + export function parseProjectToState(project: Project, selected: boolean): StateProject { const { idProject, Name } = project; @@ -100,3 +115,32 @@ export function parseFoldersToState(folders: IngestFolder[]): StateFolder[] { return stateFolders; } + +export function parseSubtitlesToState(titles: IngestTitle): SubtitleFields { + const { forced, subtitle } = titles; + const result: SubtitleFields = []; + // If forced, user is required to use the value from subtitle + if (forced && subtitle) { + result.push({ value: subtitle[0] as string, selected: true, subtitleOption: eSubtitleOption.eForced, id: 0 }) + return result; + } + + if (subtitle) { + subtitle.forEach((subtitleVal, key) => { + // Supply "None" as an option + if (subtitleVal === '') { + result.push({ value: '', selected: false, subtitleOption: eSubtitleOption.eNone, id: key }); + } + // User Input + if (subtitleVal === null) { + result.push({ value: '', selected: true, subtitleOption: eSubtitleOption.eInput, id: key }); + } + // Inherited Value + if (subtitleVal && subtitleVal !== '' && subtitleVal.length) { + result.push({ value: subtitleVal, selected: false, subtitleOption: eSubtitleOption.eInherit, id: key }); + } + }); + } + + return result; +} diff --git a/client/src/types/graphql.tsx b/client/src/types/graphql.tsx index 4eead6a1d..ce254f710 100644 --- a/client/src/types/graphql.tsx +++ b/client/src/types/graphql.tsx @@ -35,8 +35,6 @@ export type Query = { getFilterViewData: GetFilterViewDataResult; getIngestTitle: GetIngestTitleResult; getIngestionItems: GetIngestionItemsResult; - getIngestionItemsForSubjects: GetIngestionItemsForSubjectsResult; - getIngestionProjectsForSubjects: GetIngestionProjectsForSubjectsResult; getIntermediaryFile: GetIntermediaryFileResult; getItem: GetItemResult; getItemsForSubject: GetItemsForSubjectResult; @@ -131,16 +129,6 @@ export type QueryGetIngestionItemsArgs = { }; -export type QueryGetIngestionItemsForSubjectsArgs = { - input: GetIngestionItemsForSubjectsInput; -}; - - -export type QueryGetIngestionProjectsForSubjectsArgs = { - input: GetIngestionProjectsForSubjectsInput; -}; - - export type QueryGetIntermediaryFileArgs = { input: GetIntermediaryFileInput; }; @@ -2221,25 +2209,6 @@ export type SearchIngestionSubjectsResult = { SubjectUnitIdentifier: Array; }; -export type GetIngestionItemsForSubjectsInput = { - idSubjects: Array; -}; - -export type GetIngestionItemsForSubjectsResult = { - __typename?: 'GetIngestionItemsForSubjectsResult'; - Item: Array; -}; - -export type GetIngestionProjectsForSubjectsInput = { - idSubjects: Array; -}; - -export type GetIngestionProjectsForSubjectsResult = { - __typename?: 'GetIngestionProjectsForSubjectsResult'; - Project: Array; - Default: Scalars['Boolean']; -}; - export type IngestionItem = { __typename?: 'IngestionItem'; idItem: Scalars['Int']; @@ -3813,39 +3782,6 @@ export type GetIngestionItemsQuery = ( ) } ); -export type GetIngestionItemsForSubjectsQueryVariables = Exact<{ - input: GetIngestionItemsForSubjectsInput; -}>; - - -export type GetIngestionItemsForSubjectsQuery = ( - { __typename?: 'Query' } - & { getIngestionItemsForSubjects: ( - { __typename?: 'GetIngestionItemsForSubjectsResult' } - & { Item: Array<( - { __typename?: 'Item' } - & Pick - )> } - ) } -); - -export type GetIngestionProjectsForSubjectsQueryVariables = Exact<{ - input: GetIngestionProjectsForSubjectsInput; -}>; - - -export type GetIngestionProjectsForSubjectsQuery = ( - { __typename?: 'Query' } - & { getIngestionProjectsForSubjects: ( - { __typename?: 'GetIngestionProjectsForSubjectsResult' } - & Pick - & { Project: Array<( - { __typename?: 'Project' } - & Pick - )> } - ) } -); - export type GetItemQueryVariables = Exact<{ input: GetItemInput; }>; @@ -6845,84 +6781,6 @@ export function useGetIngestionItemsLazyQuery(baseOptions?: Apollo.LazyQueryHook export type GetIngestionItemsQueryHookResult = ReturnType; export type GetIngestionItemsLazyQueryHookResult = ReturnType; export type GetIngestionItemsQueryResult = Apollo.QueryResult; -export const GetIngestionItemsForSubjectsDocument = gql` - query getIngestionItemsForSubjects($input: GetIngestionItemsForSubjectsInput!) { - getIngestionItemsForSubjects(input: $input) { - Item { - idItem - EntireSubject - Name - } - } -} - `; - -/** - * __useGetIngestionItemsForSubjectsQuery__ - * - * To run a query within a React component, call `useGetIngestionItemsForSubjectsQuery` and pass it any options that fit your needs. - * When your component renders, `useGetIngestionItemsForSubjectsQuery` returns an object from Apollo Client that contains loading, error, and data properties - * you can use to render your UI. - * - * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; - * - * @example - * const { data, loading, error } = useGetIngestionItemsForSubjectsQuery({ - * variables: { - * input: // value for 'input' - * }, - * }); - */ -export function useGetIngestionItemsForSubjectsQuery(baseOptions: Apollo.QueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(GetIngestionItemsForSubjectsDocument, options); - } -export function useGetIngestionItemsForSubjectsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(GetIngestionItemsForSubjectsDocument, options); - } -export type GetIngestionItemsForSubjectsQueryHookResult = ReturnType; -export type GetIngestionItemsForSubjectsLazyQueryHookResult = ReturnType; -export type GetIngestionItemsForSubjectsQueryResult = Apollo.QueryResult; -export const GetIngestionProjectsForSubjectsDocument = gql` - query getIngestionProjectsForSubjects($input: GetIngestionProjectsForSubjectsInput!) { - getIngestionProjectsForSubjects(input: $input) { - Project { - idProject - Name - } - Default - } -} - `; - -/** - * __useGetIngestionProjectsForSubjectsQuery__ - * - * To run a query within a React component, call `useGetIngestionProjectsForSubjectsQuery` and pass it any options that fit your needs. - * When your component renders, `useGetIngestionProjectsForSubjectsQuery` returns an object from Apollo Client that contains loading, error, and data properties - * you can use to render your UI. - * - * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; - * - * @example - * const { data, loading, error } = useGetIngestionProjectsForSubjectsQuery({ - * variables: { - * input: // value for 'input' - * }, - * }); - */ -export function useGetIngestionProjectsForSubjectsQuery(baseOptions: Apollo.QueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(GetIngestionProjectsForSubjectsDocument, options); - } -export function useGetIngestionProjectsForSubjectsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(GetIngestionProjectsForSubjectsDocument, options); - } -export type GetIngestionProjectsForSubjectsQueryHookResult = ReturnType; -export type GetIngestionProjectsForSubjectsLazyQueryHookResult = ReturnType; -export type GetIngestionProjectsForSubjectsQueryResult = Apollo.QueryResult; export const GetItemDocument = gql` query getItem($input: GetItemInput!) { getItem(input: $input) { diff --git a/server/graphql/api/index.ts b/server/graphql/api/index.ts index 2efad6feb..a555a017a 100644 --- a/server/graphql/api/index.ts +++ b/server/graphql/api/index.ts @@ -61,10 +61,6 @@ import { CreateVocabularySetResult, SearchIngestionSubjectsInput, SearchIngestionSubjectsResult, - GetIngestionItemsForSubjectsInput, - GetIngestionItemsForSubjectsResult, - GetIngestionProjectsForSubjectsInput, - GetIngestionProjectsForSubjectsResult, GetVocabularyEntriesInput, GetVocabularyEntriesResult, GetContentsForAssetVersionsInput, @@ -110,7 +106,9 @@ import { GetUnitsFromNameSearchResult, GetUnitsFromNameSearchInput, GetProjectListResult, - GetProjectListInput + GetProjectListInput, + GetIngestionItemsInput, + GetIngestionItemsResult } from '../../types/graphql'; // Queries @@ -133,8 +131,6 @@ import getWorkflow from './queries/workflow/getWorkflow'; import getWorkflowList from './queries/workflow/getWorkflowList'; import getUploadedAssetVersion from './queries/asset/getUploadedAssetVersion'; import searchIngestionSubjects from './queries/unit/searchIngestionSubjects'; -import getIngestionItemsForSubjects from './queries/unit/getIngestionItemsForSubjects'; -import getIngestionProjectsForSubjects from './queries/unit/getIngestionProjectsForSubjects'; import getVocabularyEntries from './queries/vocabulary/getVocabularyEntries'; import getContentsForAssetVersions from './queries/asset/getContentsForAssetVersions'; import getModelConstellationForAssetVersion from './queries/asset/getModelConstellationForAssetVersion'; @@ -202,8 +198,6 @@ const allQueries = { uploadAsset, getUploadedAssetVersion, searchIngestionSubjects, - getIngestionItemsForSubjects, - getIngestionProjectsForSubjects, getVocabularyEntries, getContentsForAssetVersions, getModelConstellationForAssetVersion, @@ -403,26 +397,6 @@ class GraphQLApi { }); } - async getIngestionItemsForSubjects(input: GetIngestionItemsForSubjectsInput, context?: Context): Promise { - const operationName = 'getIngestionItemsForSubjects'; - const variables = { input }; - return this.graphqlRequest({ - operationName, - variables, - context - }); - } - - async getIngestionProjectsForSubjects(input: GetIngestionProjectsForSubjectsInput, context?: Context): Promise { - const operationName = 'getIngestionProjectsForSubjects'; - const variables = { input }; - return this.graphqlRequest({ - operationName, - variables, - context - }); - } - async getVocabularyEntries(input: GetVocabularyEntriesInput, context?: Context): Promise { const operationName = 'getVocabularyEntries'; const variables = { input }; @@ -783,6 +757,16 @@ class GraphQLApi { }); } + async getIngestionItems(input: GetIngestionItemsInput, context?: Context): Promise { + const operationName = 'getIngestionItems'; + const variables = { input }; + return this.graphqlRequest({ + operationName, + variables, + context + }) + } + private async graphqlRequest({ query, variables, context, operationName }: GraphQLRequest): Promise { const queryNode: DocumentNode = allQueries[operationName]; const queryNodeString: string = print(queryNode); diff --git a/server/graphql/api/queries/unit/getIngestionItemsForSubjects.ts b/server/graphql/api/queries/unit/getIngestionItemsForSubjects.ts deleted file mode 100644 index 94acf25bb..000000000 --- a/server/graphql/api/queries/unit/getIngestionItemsForSubjects.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { gql } from 'apollo-server-express'; - -const getIngestionItemsForSubjects = gql` - query getIngestionItemsForSubjects($input: GetIngestionItemsForSubjectsInput!) { - getIngestionItemsForSubjects(input: $input) { - Item { - idItem - EntireSubject - Name - } - } - } -`; - -export default getIngestionItemsForSubjects; diff --git a/server/graphql/api/queries/unit/getIngestionProjectsForSubjects.ts b/server/graphql/api/queries/unit/getIngestionProjectsForSubjects.ts deleted file mode 100644 index ffad038ff..000000000 --- a/server/graphql/api/queries/unit/getIngestionProjectsForSubjects.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { gql } from 'apollo-server-express'; - -const getIngestionProjectsForSubjects = gql` - query getIngestionProjectsForSubjects($input: GetIngestionProjectsForSubjectsInput!) { - getIngestionProjectsForSubjects(input: $input) { - Project { - idProject - Name - } - Default - } - } -`; - -export default getIngestionProjectsForSubjects; diff --git a/server/graphql/schema.graphql b/server/graphql/schema.graphql index 23a64038a..b5dc1f650 100644 --- a/server/graphql/schema.graphql +++ b/server/graphql/schema.graphql @@ -14,8 +14,6 @@ type Query { getFilterViewData: GetFilterViewDataResult! getIngestTitle(input: GetIngestTitleInput!): GetIngestTitleResult! getIngestionItems(input: GetIngestionItemsInput!): GetIngestionItemsResult! - getIngestionItemsForSubjects(input: GetIngestionItemsForSubjectsInput!): GetIngestionItemsForSubjectsResult! - getIngestionProjectsForSubjects(input: GetIngestionProjectsForSubjectsInput!): GetIngestionProjectsForSubjectsResult! getIntermediaryFile(input: GetIntermediaryFileInput!): GetIntermediaryFileResult! getItem(input: GetItemInput!): GetItemResult! getItemsForSubject(input: GetItemsForSubjectInput!): GetItemsForSubjectResult! @@ -1727,23 +1725,6 @@ type SearchIngestionSubjectsResult { SubjectUnitIdentifier: [SubjectUnitIdentifier!]! } -input GetIngestionItemsForSubjectsInput { - idSubjects: [Int!]! -} - -type GetIngestionItemsForSubjectsResult { - Item: [Item!]! -} - -input GetIngestionProjectsForSubjectsInput { - idSubjects: [Int!]! -} - -type GetIngestionProjectsForSubjectsResult { - Project: [Project!]! - Default: Boolean! -} - type IngestionItem { idItem: Int! EntireSubject: Boolean! diff --git a/server/graphql/schema/ingestion/resolvers/mutations/ingestData.ts b/server/graphql/schema/ingestion/resolvers/mutations/ingestData.ts index cd638f3fd..09053f66d 100644 --- a/server/graphql/schema/ingestion/resolvers/mutations/ingestData.ts +++ b/server/graphql/schema/ingestion/resolvers/mutations/ingestData.ts @@ -24,6 +24,7 @@ import { getRelatedObjects } from '../../../systemobject/resolvers/queries/getSy import { PublishScene } from '../../../../../collections/impl/PublishScene'; import { NameHelpers, ModelHierarchy } from '../../../../../utils/nameHelpers'; import * as COMMON from '@dpo-packrat/common'; +import { eSystemObjectType } from '@dpo-packrat/common'; type AssetPair = { asset: DBAPI.Asset; @@ -1476,6 +1477,8 @@ class IngestDataWorker extends ResolverBase { if (this.ingestScene) { for (const scene of this.input.scene) { // add validation in this area while we iterate through the objects + if (!scene.sourceObjects || !scene.sourceObjects.length || !scene.sourceObjects.some(sourceObj => sourceObj.objectType === eSystemObjectType.eModel)) + return { success: false, error: 'Scene ingestion must have at least 1 source object of type model' }; if (scene.sourceObjects && scene.sourceObjects.length) { for (const sourceObject of scene.sourceObjects) { if (!isValidParentChildRelationship(sourceObject.objectType, COMMON.eSystemObjectType.eScene, scene.sourceObjects, [], true)) { diff --git a/server/graphql/schema/unit/queries.graphql b/server/graphql/schema/unit/queries.graphql index 15a5031d3..24a93001e 100644 --- a/server/graphql/schema/unit/queries.graphql +++ b/server/graphql/schema/unit/queries.graphql @@ -3,8 +3,6 @@ type Query { getItemsForSubject(input: GetItemsForSubjectInput!): GetItemsForSubjectResult! getObjectsForItem(input: GetObjectsForItemInput!): GetObjectsForItemResult! searchIngestionSubjects(input: SearchIngestionSubjectsInput!): SearchIngestionSubjectsResult! - getIngestionItemsForSubjects(input: GetIngestionItemsForSubjectsInput!): GetIngestionItemsForSubjectsResult! - getIngestionProjectsForSubjects(input: GetIngestionProjectsForSubjectsInput!): GetIngestionProjectsForSubjectsResult! getIngestionItems(input: GetIngestionItemsInput!): GetIngestionItemsResult! getUnit(input: GetUnitInput!): GetUnitResult! getProject(input: GetProjectInput!): GetProjectResult! @@ -64,23 +62,6 @@ type SearchIngestionSubjectsResult { SubjectUnitIdentifier: [SubjectUnitIdentifier!]! } -input GetIngestionItemsForSubjectsInput { - idSubjects: [Int!]! -} - -type GetIngestionItemsForSubjectsResult { - Item: [Item!]! -} - -input GetIngestionProjectsForSubjectsInput { - idSubjects: [Int!]! -} - -type GetIngestionProjectsForSubjectsResult { - Project: [Project!]! - Default: Boolean! -} - type IngestionItem { idItem: Int! EntireSubject: Boolean! diff --git a/server/graphql/schema/unit/resolvers/index.ts b/server/graphql/schema/unit/resolvers/index.ts index 1a740705f..b2042174d 100644 --- a/server/graphql/schema/unit/resolvers/index.ts +++ b/server/graphql/schema/unit/resolvers/index.ts @@ -13,8 +13,6 @@ import createUnit from './mutations/createUnit'; import createProject from './mutations/createProject'; import createSubject from './mutations/createSubject'; import searchIngestionSubjects from './queries/searchIngestionSubjects'; -import getIngestionProjectsForSubjects from './queries/getIngestionProjectsForSubjects'; -import getIngestionItemsForSubjects from './queries/getIngestionItemsForSubjects'; import getIngestionItems from './queries/getIngestionItems'; import getSubjectsForUnit from './queries/getSubjectsForUnit'; import getItemsForSubject from './queries/getItemsForSubject'; @@ -32,8 +30,6 @@ const resolvers = { getSubject, getItem, searchIngestionSubjects, - getIngestionProjectsForSubjects, - getIngestionItemsForSubjects, getIngestionItems, getSubjectsForUnit, getItemsForSubject, diff --git a/server/graphql/schema/unit/resolvers/queries/getIngestionItems.ts b/server/graphql/schema/unit/resolvers/queries/getIngestionItems.ts index e36fc34e5..1083d9c66 100644 --- a/server/graphql/schema/unit/resolvers/queries/getIngestionItems.ts +++ b/server/graphql/schema/unit/resolvers/queries/getIngestionItems.ts @@ -4,7 +4,7 @@ import * as DBAPI from '../../../../../db'; import * as LOG from '../../../../../utils/logger'; import * as H from '../../../../../utils/helpers'; -export default async function getIngestionItemsForSubjects(_: Parent, args: QueryGetIngestionItemsArgs): Promise { +export default async function getIngestionItems(_: Parent, args: QueryGetIngestionItemsArgs): Promise { const { input } = args; const { idSubjects } = input; diff --git a/server/graphql/schema/unit/resolvers/queries/getIngestionItemsForSubjects.ts b/server/graphql/schema/unit/resolvers/queries/getIngestionItemsForSubjects.ts deleted file mode 100644 index d3072f0f5..000000000 --- a/server/graphql/schema/unit/resolvers/queries/getIngestionItemsForSubjects.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { QueryGetIngestionItemsForSubjectsArgs, GetIngestionItemsForSubjectsResult } from '../../../../../types/graphql'; -import { Parent } from '../../../../../types/resolvers'; -import * as DBAPI from '../../../../../db'; - -export default async function getIngestionItemsForSubjects(_: Parent, args: QueryGetIngestionItemsForSubjectsArgs): Promise { - const { input } = args; - const { idSubjects } = input; - - const Item: DBAPI.Item[] = await DBAPI.Item.fetchDerivedFromSubjects(idSubjects) ?? []; - return { Item }; -} diff --git a/server/graphql/schema/unit/resolvers/queries/getIngestionProjectsForSubjects.ts b/server/graphql/schema/unit/resolvers/queries/getIngestionProjectsForSubjects.ts deleted file mode 100644 index db80b73da..000000000 --- a/server/graphql/schema/unit/resolvers/queries/getIngestionProjectsForSubjects.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { QueryGetIngestionProjectsForSubjectsArgs, GetIngestionProjectsForSubjectsResult } from '../../../../../types/graphql'; -import { Parent } from '../../../../../types/resolvers'; -import * as DBAPI from '../../../../../db'; - -export default async function getIngestionProjectsForSubjects(_: Parent, args: QueryGetIngestionProjectsForSubjectsArgs): Promise { - const { input } = args; - const { idSubjects } = input; - - const Project = await DBAPI.Project.fetchRelatedToSubjects(idSubjects); - - if (Project) { - if (Project.length) { - return { - Project, - Default: false - }; - } - } - - const AllProjects = await DBAPI.Project.fetchAll(); - - if (AllProjects) { - return { - Project: AllProjects, - Default: true - }; - } - - return { - Project: [], - Default: true - }; -} diff --git a/server/tests/graphql/graphql.test.ts b/server/tests/graphql/graphql.test.ts index ada43c6c3..89fcfd3a0 100644 --- a/server/tests/graphql/graphql.test.ts +++ b/server/tests/graphql/graphql.test.ts @@ -19,8 +19,6 @@ import getUnitTest from './queries/unit/getUnit.test'; import getVocabularyTest from './queries/vocabulary/getVocabulary.test'; import getWorkflowTest from './queries/workflow/getWorkflow.test'; import getVocabularyEntriesTest from './queries/vocabulary/getVocabularyEntries.test'; -import getIngestionItemsForSubjectsTest from './queries/unit/getIngestionItemsForSubjects.test'; -import getIngestionProjectsForSubjectsTest from './queries/unit/getIngestionProjectsForSubjects.test'; import searchIngestionSubjectsTest from './queries/unit/searchIngestionSubjects.test'; import areCameraSettingsUniformTest from './queries/ingestion/areCameraSettingsUniform.test'; import getContentsForAssetVersionsTest from './queries/asset/getContentsForAssetVersions.test'; @@ -68,8 +66,6 @@ describe('GraphQL Test Suite', () => { getVocabularyTest(utils); getWorkflowTest(utils); getVocabularyEntriesTest(utils); - getIngestionItemsForSubjectsTest(utils); - getIngestionProjectsForSubjectsTest(utils); searchIngestionSubjectsTest(utils); areCameraSettingsUniformTest(utils); getContentsForAssetVersionsTest(utils); diff --git a/server/tests/graphql/mutations/ingestion/ingestData.test.ts b/server/tests/graphql/mutations/ingestion/ingestData.test.ts index b6f32013e..17e6d9ea3 100644 --- a/server/tests/graphql/mutations/ingestion/ingestData.test.ts +++ b/server/tests/graphql/mutations/ingestion/ingestData.test.ts @@ -2,7 +2,6 @@ import GraphQLApi from '../../../../graphql'; import TestSuiteUtils from '../../utils'; import { IngestItemInput, - IngestProjectInput, IngestSubjectInput, IngestPhotogrammetry, IngestIdentifier, @@ -13,7 +12,8 @@ import { CreateUserInput, GetContentsForAssetVersionsInput, CreateVocabularyInput, - CreateVocabularySetInput + CreateVocabularySetInput, + IngestProjectInput } from '../../../../types/graphql'; import { Context } from '../../../../types/resolvers'; import * as COMMON from '@dpo-packrat/common'; @@ -91,17 +91,19 @@ const ingestDataTest = (utils: TestSuiteUtils): void => { unit: UnitAbbreviation }; - const projectsInput = { + const ingestionItemsInput = { idSubjects: [idSubject] }; - const { Project } = await graphQLApi.getIngestionProjectsForSubjects(projectsInput); - expect(Project).toBeTruthy(); - const { idProject, Name } = Project[0]; + const { IngestionItem } = await graphQLApi.getIngestionItems(ingestionItemsInput); + expect(IngestionItem).toBeTruthy(); + + if (!IngestionItem || !IngestionItem.length) done(); + const project: IngestProjectInput = { - id: idProject, - name: Name + id: IngestionItem?.[0].idProject ?? 0, + name: IngestionItem?.[0].ProjectName ?? '' }; const item: IngestItemInput = { diff --git a/server/tests/graphql/queries/unit/getIngestionItemsForSubjects.test.ts b/server/tests/graphql/queries/unit/getIngestionItemsForSubjects.test.ts deleted file mode 100644 index c834a4a0e..000000000 --- a/server/tests/graphql/queries/unit/getIngestionItemsForSubjects.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import GraphQLApi from '../../../../graphql'; -import TestSuiteUtils from '../../utils'; -import { CreateUnitInput, CreateSubjectInput } from '../../../../types/graphql'; - -const getIngestionItemsForSubjectsTest = (utils: TestSuiteUtils): void => { - let graphQLApi: GraphQLApi; - let createUnitInput: () => CreateUnitInput; - let createSubjectInput: (idUnit: number) => CreateSubjectInput; - - beforeAll(() => { - graphQLApi = utils.graphQLApi; - createUnitInput = utils.createUnitInput; - createSubjectInput = utils.createSubjectInput; - }); - - describe('Query: getIngestionItemsForSubjects', () => { - test('should work with valid input', async () => { - const unitInput = createUnitInput(); - const { Unit } = await graphQLApi.createUnit(unitInput); - - if (Unit) { - const subjectInput = createSubjectInput(Unit.idUnit); - const { Subject } = await graphQLApi.createSubject(subjectInput); - - if (Subject) { - const input = { - idSubjects: [Subject.idSubject] - }; - const { Item } = await graphQLApi.getIngestionItemsForSubjects(input); - expect(Item).toBeTruthy(); - } - } - }); - }); -}; - -export default getIngestionItemsForSubjectsTest; diff --git a/server/tests/graphql/queries/unit/getIngestionProjectsForSubjects.test.ts b/server/tests/graphql/queries/unit/getIngestionProjectsForSubjects.test.ts deleted file mode 100644 index 6e7cfd9c0..000000000 --- a/server/tests/graphql/queries/unit/getIngestionProjectsForSubjects.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import GraphQLApi from '../../../../graphql'; -import TestSuiteUtils from '../../utils'; -import { CreateUnitInput, CreateSubjectInput } from '../../../../types/graphql'; - -const getIngestionProjectsForSubjectsTest = (utils: TestSuiteUtils): void => { - let graphQLApi: GraphQLApi; - let createUnitInput: () => CreateUnitInput; - let createSubjectInput: (idUnit: number) => CreateSubjectInput; - - beforeAll(() => { - graphQLApi = utils.graphQLApi; - createUnitInput = utils.createUnitInput; - createSubjectInput = utils.createSubjectInput; - }); - - describe('Query: getIngestionProjectsForSubjects', () => { - test('should work with valid input', async () => { - const unitInput = createUnitInput(); - const { Unit } = await graphQLApi.createUnit(unitInput); - - if (Unit) { - const subjectInput = createSubjectInput(Unit.idUnit); - const { Subject } = await graphQLApi.createSubject(subjectInput); - - if (Subject) { - const input = { - idSubjects: [Subject.idSubject] - }; - const { Project } = await graphQLApi.getIngestionProjectsForSubjects(input); - expect(Project).toBeTruthy(); - } - } - }); - }); -}; - -export default getIngestionProjectsForSubjectsTest; diff --git a/server/types/graphql.ts b/server/types/graphql.ts index 52c1f03a8..0bffc581c 100644 --- a/server/types/graphql.ts +++ b/server/types/graphql.ts @@ -32,8 +32,6 @@ export type Query = { getFilterViewData: GetFilterViewDataResult; getIngestTitle: GetIngestTitleResult; getIngestionItems: GetIngestionItemsResult; - getIngestionItemsForSubjects: GetIngestionItemsForSubjectsResult; - getIngestionProjectsForSubjects: GetIngestionProjectsForSubjectsResult; getIntermediaryFile: GetIntermediaryFileResult; getItem: GetItemResult; getItemsForSubject: GetItemsForSubjectResult; @@ -128,16 +126,6 @@ export type QueryGetIngestionItemsArgs = { }; -export type QueryGetIngestionItemsForSubjectsArgs = { - input: GetIngestionItemsForSubjectsInput; -}; - - -export type QueryGetIngestionProjectsForSubjectsArgs = { - input: GetIngestionProjectsForSubjectsInput; -}; - - export type QueryGetIntermediaryFileArgs = { input: GetIntermediaryFileInput; }; @@ -2218,25 +2206,6 @@ export type SearchIngestionSubjectsResult = { SubjectUnitIdentifier: Array; }; -export type GetIngestionItemsForSubjectsInput = { - idSubjects: Array; -}; - -export type GetIngestionItemsForSubjectsResult = { - __typename?: 'GetIngestionItemsForSubjectsResult'; - Item: Array; -}; - -export type GetIngestionProjectsForSubjectsInput = { - idSubjects: Array; -}; - -export type GetIngestionProjectsForSubjectsResult = { - __typename?: 'GetIngestionProjectsForSubjectsResult'; - Project: Array; - Default: Scalars['Boolean']; -}; - export type IngestionItem = { __typename?: 'IngestionItem'; idItem: Scalars['Int']; diff --git a/server/utils/nameHelpers.ts b/server/utils/nameHelpers.ts index be2305daa..ebf97a85b 100644 --- a/server/utils/nameHelpers.ts +++ b/server/utils/nameHelpers.ts @@ -154,9 +154,11 @@ export class NameHelpers { const subtitle: (string | null)[] = []; if (subject !== null) { - subtitle.push([...subtitleSet].join(', ')); - subtitle.push(''); - subtitle.push(null); + const mergedSubtitle: string = [...subtitleSet].join(', '); + if (mergedSubtitle) + subtitle.push(mergedSubtitle); + subtitle.push(''); + subtitle.push(null); } else subtitle.push(null); return { title: subject?.Name ?? null, subtitle }; From 5e6789cdf0580dd5b7d2f7a4ede8621b3fc322d7 Mon Sep 17 00:00:00 2001 From: Hsin Tung Date: Sat, 26 Mar 2022 01:09:48 -0700 Subject: [PATCH 10/31] allow for subtitle updates --- client/src/components/controls/InputField.tsx | 5 +-- .../DetailsView/DetailsTab/ItemDetails.tsx | 6 ++-- .../DetailsView/DetailsTab/ModelDetails.tsx | 29 ++++++++++++++-- .../DetailsView/DetailsTab/SceneDetails.tsx | 19 ++++++++--- .../DetailsView/DetailsTab/SubjectDetails.tsx | 33 +++++++++++++++---- .../DetailsView/DetailsTab/index.tsx | 25 ++++++++++++-- .../components/DetailsView/index.tsx | 27 +++++++++++---- .../src/store/metadata/metadata.defaults.ts | 1 + client/src/types/graphql.tsx | 3 +- .../systemobject/getSystemObjectDetails.ts | 1 + 10 files changed, 122 insertions(+), 27 deletions(-) diff --git a/client/src/components/controls/InputField.tsx b/client/src/components/controls/InputField.tsx index 15bddfc3e..7472e0c51 100644 --- a/client/src/components/controls/InputField.tsx +++ b/client/src/components/controls/InputField.tsx @@ -41,13 +41,14 @@ interface InputFieldProps extends ViewableProps { padding?: string; inputHeight?: string; gridGap?: string; + containerStyle?: any; } function InputField(props: InputFieldProps): React.ReactElement { - const { label, name, value, onChange, type, required = false, viewMode = false, disabled = false, valueLeftAligned = false, gridLabel, gridValue, padding, gridGap } = props; + const { label, name, value, onChange, type, required = false, viewMode = false, disabled = false, valueLeftAligned = false, gridLabel, gridValue, padding, gridGap, containerStyle } = props; const classes = useStyles(props); - const rowFieldProps = { alignItems: 'center', justifyContent: 'space-between', style: { borderRadius: 0 } }; + const rowFieldProps = { alignItems: 'center', justifyContent: 'space-between', style: { borderRadius: 0, ...containerStyle } }; return ( [state.ItemDetails, state.updateDetailField]); - + useEffect(() => { onUpdateDetail(objectType, ItemDetails); }, [ItemDetails]); @@ -49,6 +49,8 @@ function ItemDetails(props: DetailComponentProps): React.ReactElement { originalFields={itemData} disabled={disabled} onChange={onSetField} + subtitle={subtitle} + onSubtitleUpdate={onSubtitleUpdate} isItemView setCheckboxField={setCheckboxField} ItemDetails={ItemDetails} diff --git a/client/src/pages/Repository/components/DetailsView/DetailsTab/ModelDetails.tsx b/client/src/pages/Repository/components/DetailsView/DetailsTab/ModelDetails.tsx index 98eef0159..bdd48da6c 100644 --- a/client/src/pages/Repository/components/DetailsView/DetailsTab/ModelDetails.tsx +++ b/client/src/pages/Repository/components/DetailsView/DetailsTab/ModelDetails.tsx @@ -6,7 +6,7 @@ * * This component renders details tab for Model specific details used in DetailsTab component. */ -import { Typography, Box, makeStyles, Select, MenuItem } from '@material-ui/core'; +import { Typography, Box, makeStyles, Select, MenuItem, fade } from '@material-ui/core'; import React, { useEffect } from 'react'; import { DateInputField, Loader, ReadOnlyRow } from '../../../../../components'; import { useVocabularyStore, useDetailTabStore } from '../../../../../store'; @@ -15,8 +15,9 @@ import { extractModelConstellation } from '../../../../../constants/helperfuncti import ObjectMeshTable from './../../../../Ingestion/components/Metadata/Model/ObjectMeshTable'; import { DetailComponentProps } from './index'; import { useStyles as useSelectStyles, SelectFieldProps } from '../../../../../components/controls/SelectField'; +import { DebounceInput } from 'react-debounce-input'; -export const useStyles = makeStyles(({ palette }) => ({ +export const useStyles = makeStyles(({ palette, typography }) => ({ value: { fontSize: '0.8em', color: palette.primary.dark @@ -88,12 +89,24 @@ export const useStyles = makeStyles(({ palette }) => ({ }, label: { color: 'auto' + }, + input: { + width: 'fit-content', + border: `1px solid ${fade(palette.primary.contrastText, 0.4)}`, + backgroundColor: palette.background.paper, + padding: 9, + borderRadius: 5, + fontWeight: typography.fontWeightRegular, + fontFamily: typography.fontFamily, + fontSize: '0.8em', + height: 3 } + })); function ModelDetails(props: DetailComponentProps): React.ReactElement { const classes = useStyles(); - const { data, loading, onUpdateDetail, objectType } = props; + const { data, loading, onUpdateDetail, objectType, subtitle, onSubtitleUpdate } = props; const { ingestionModel, modelObjects } = extractModelConstellation(data?.getDetailsTabDataForObject?.Model); const [ModelDetails, updateDetailField] = useDetailTabStore(state => [state.ModelDetails, state.updateDetailField]); @@ -133,6 +146,16 @@ function ModelDetails(props: DetailComponentProps): React.ReactElement { +
+
+ + Subtitle + +
+
+ +
+
diff --git a/client/src/pages/Repository/components/DetailsView/DetailsTab/SceneDetails.tsx b/client/src/pages/Repository/components/DetailsView/DetailsTab/SceneDetails.tsx index 56876585a..cbb89e13b 100644 --- a/client/src/pages/Repository/components/DetailsView/DetailsTab/SceneDetails.tsx +++ b/client/src/pages/Repository/components/DetailsView/DetailsTab/SceneDetails.tsx @@ -10,7 +10,7 @@ import { Loader } from '../../../../../components'; import { GetSceneDocument } from '../../../../../types/graphql'; import { DetailComponentProps } from './index'; import { apolloClient } from '../../../../../graphql/index'; -import { ReadOnlyRow, CheckboxField } from '../../../../../components/index'; +import { ReadOnlyRow, CheckboxField, InputField } from '../../../../../components/index'; import { useDetailTabStore } from '../../../../../store'; import { eSystemObjectType } from '@dpo-packrat/common'; @@ -36,7 +36,7 @@ export const useStyles = makeStyles(({ palette }) => ({ function SceneDetails(props: DetailComponentProps): React.ReactElement { const classes = useStyles(); const isMounted = useRef(false); - const { data, loading, onUpdateDetail, objectType } = props; + const { data, loading, onUpdateDetail, objectType, subtitle, onSubtitleUpdate } = props; const [SceneDetails, updateDetailField] = useDetailTabStore(state => [state.SceneDetails, state.updateDetailField]); useEffect(() => { @@ -84,18 +84,26 @@ function SceneDetails(props: DetailComponentProps): React.ReactElement { updateDetailField(eSystemObjectType.eScene, name, checked); }; - // console.log(`SceneDetails = ${JSON.stringify(SceneDetails)}`); return ( + diff --git a/client/src/pages/Repository/components/DetailsView/DetailsTab/SubjectDetails.tsx b/client/src/pages/Repository/components/DetailsView/DetailsTab/SubjectDetails.tsx index f421b5a28..635b84726 100644 --- a/client/src/pages/Repository/components/DetailsView/DetailsTab/SubjectDetails.tsx +++ b/client/src/pages/Repository/components/DetailsView/DetailsTab/SubjectDetails.tsx @@ -49,15 +49,35 @@ interface SubjectFieldsProps extends SubjectDetailFields { onChange: (event: React.ChangeEvent) => void; isItemView?: boolean; setCheckboxField?: (event) => void; - // TOFIX? ItemDetails?: any; itemData?: any; + subtitle?: string; + onSubtitleUpdate?: (event: React.ChangeEvent) => void; } export function SubjectFields(props: SubjectFieldsProps): React.ReactElement { - const { originalFields, Latitude, Longitude, Altitude, TS0, TS1, TS2, R0, R1, R2, R3, disabled, onChange, isItemView = false, setCheckboxField, ItemDetails, itemData } = props; + const { + originalFields, + Latitude, + Longitude, + Altitude, + TS0, + TS1, + TS2, + R0, + R1, + R2, + R3, + disabled, + onChange, + isItemView = false, + setCheckboxField, + ItemDetails, + itemData, + subtitle, + onSubtitleUpdate + } = props; const classes = useStyles(); - const details = { Latitude, Longitude, @@ -68,7 +88,8 @@ export function SubjectFields(props: SubjectFieldsProps): React.ReactElement { R0, R1, R2, - R3 + R3, + subtitle }; return ( @@ -87,10 +108,10 @@ export function SubjectFields(props: SubjectFieldsProps): React.ReactElement { ) => void} className={clsx(classes.input, classes.datasetFieldInput)} style={{ height: 22, diff --git a/client/src/pages/Repository/components/DetailsView/DetailsTab/index.tsx b/client/src/pages/Repository/components/DetailsView/DetailsTab/index.tsx index 637cd9c61..a75cbf5b0 100644 --- a/client/src/pages/Repository/components/DetailsView/DetailsTab/index.tsx +++ b/client/src/pages/Repository/components/DetailsView/DetailsTab/index.tsx @@ -82,6 +82,8 @@ const useStyles = makeStyles(({ palette }) => ({ export interface DetailComponentProps extends GetDetailsTabDataForObjectQueryResult { disabled: boolean; objectType: number; + subtitle: string; + onSubtitleUpdate: (e) => void; onUpdateDetail: (objectType: number, data: UpdateDataFields) => void; } @@ -109,13 +111,30 @@ type DetailsTabParams = { onAddSourceObject: () => void; onAddDerivedObject: () => void; onUpdateDetail: (objectType: number, data: UpdateDataFields) => void; + onSubtitleUpdate: (e) => void; + subtitle?: string; objectVersions: SystemObjectVersion[]; detailQuery: any; metadata: Metadata[] }; function DetailsTab(props: DetailsTabParams): React.ReactElement { - const { disabled, idSystemObject, objectType, assetOwner, sourceObjects, derivedObjects, onAddSourceObject, onAddDerivedObject, onUpdateDetail, objectVersions, detailQuery, metadata } = props; + const { + disabled, + idSystemObject, + objectType, + assetOwner, + sourceObjects, + derivedObjects, + onAddSourceObject, + onAddDerivedObject, + onUpdateDetail, + onSubtitleUpdate, + subtitle, + objectVersions, + detailQuery, + metadata + } = props; const [tab, setTab] = useState(0); const classes = useStyles(); const history = useHistory(); @@ -193,7 +212,9 @@ function DetailsTab(props: DetailsTabParams): React.ReactElement { const sharedProps = { onUpdateDetail, objectType, - disabled + disabled, + onSubtitleUpdate, + subtitle }; const detailsProps = { diff --git a/client/src/pages/Repository/components/DetailsView/index.tsx b/client/src/pages/Repository/components/DetailsView/index.tsx index 9f2a222bb..7b9dec340 100644 --- a/client/src/pages/Repository/components/DetailsView/index.tsx +++ b/client/src/pages/Repository/components/DetailsView/index.tsx @@ -13,7 +13,7 @@ import { useParams } from 'react-router'; import { toast } from 'react-toastify'; import { LoadingButton } from '../../../../components'; import IdentifierList from '../../../../components/shared/IdentifierList'; -import { /*parseIdentifiersToState,*/ useVocabularyStore, useRepositoryStore, useIdentifierStore, useDetailTabStore, ModelDetailsType, SceneDetailsType, useObjectMetadataStore, eObjectMetadataType } from '../../../../store'; +import { useVocabularyStore, useRepositoryStore, useIdentifierStore, useDetailTabStore, ModelDetailsType, SceneDetailsType, useObjectMetadataStore, eObjectMetadataType } from '../../../../store'; import { ActorDetailFieldsInput, AssetDetailFieldsInput, @@ -75,6 +75,7 @@ type DetailsFields = { name?: string; retired?: boolean; idLicense?: number; + subtitle?: string; }; @@ -143,8 +144,8 @@ function DetailsView(): React.ReactElement { useEffect(() => { if (data && !loading) { - const { name, retired, license, metadata } = data.getSystemObjectDetails; - setDetails({ name, retired, idLicense: license?.idLicense || 0 }); + const { name, retired, license, metadata, subTitle } = data.getSystemObjectDetails; + setDetails({ name, retired, idLicense: license?.idLicense || 0, subtitle: subTitle ?? '' }); initializeIdentifierState(data.getSystemObjectDetails.identifiers); if (objectType === eSystemObjectType.eSubject) { initializeMetadata(eObjectMetadataType.eSubjectView, metadata); // comment me out! @@ -267,6 +268,13 @@ function DetailsView(): React.ReactElement { setUpdatedData(updatedDataFields); }; + const onSubtitleUpdate = ({ target }): void => { + const updatedDataFields: UpdateObjectDetailsDataInput = { ...updatedData }; + setDetails(details => ({ ...details, subtitle: target.value })); + updatedDataFields.Subtitle = target.value; + setUpdatedData(updatedDataFields); + } + const onUpdateDetail = (objectType: number, data: UpdateDataFields): void => { // console.log('onUpdateDetail', objectType, data); const updatedDataFields: UpdateObjectDetailsDataInput = { @@ -352,6 +360,14 @@ function DetailsView(): React.ReactElement { return false; } + if (objectType === eSystemObjectType.eItem && typeof updatedData.Item?.EntireSubject === 'boolean' && !updatedData.Item.EntireSubject) { + if (!updatedData.Subtitle || !updatedData.Subtitle.length) { + toast.error('Subtitle required because Entire Subject is not checked'); + setIsUpdatingData(false); + return false; + } + } + // Create another validation here to make sure that the appropriate SO types are being checked const errors = await getDetailsViewFieldErrors(updatedData, objectType); if (errors.length) { @@ -361,8 +377,6 @@ function DetailsView(): React.ReactElement { } try { - // TODO: Model, Scene, and CD are currently updating in a way that - // requires the fields to be populated. if (objectType === eSystemObjectType.eModel) { const ModelDetails = getDetail(objectType) as ModelDetailsType; const { DateCreated, idVCreationMethod, idVModality, idVPurpose, idVUnits, idVFileType } = ModelDetails; @@ -455,7 +469,6 @@ function DetailsView(): React.ReactElement { const metadata = getAllMetadataEntries().filter(entry => entry.Name); updatedData.Metadata = metadata; - const { data } = await updateDetailsTabData(idSystemObject, idObject, objectType, updatedData); if (data?.updateObjectDetails?.success) { const message: string | null | undefined = data?.updateObjectDetails?.message; @@ -542,6 +555,8 @@ function DetailsView(): React.ReactElement { onAddSourceObject={onAddSourceObject} onAddDerivedObject={onAddDerivedObject} onUpdateDetail={onUpdateDetail} + onSubtitleUpdate={onSubtitleUpdate} + subtitle={details?.subtitle} objectVersions={objectVersions} detailQuery={detailQuery} metadata={metadata} diff --git a/client/src/store/metadata/metadata.defaults.ts b/client/src/store/metadata/metadata.defaults.ts index 0d1765880..4a1e567de 100644 --- a/client/src/store/metadata/metadata.defaults.ts +++ b/client/src/store/metadata/metadata.defaults.ts @@ -57,6 +57,7 @@ const notesWhenUpdate = { const selectedSubtitleValidation = { test: array => { const selectedSubtitle = array.find(subtitle => subtitle.selected); + if (!selectedSubtitle) return false; if (selectedSubtitle.subtitleOption !== eSubtitleOption.eNone) return !!selectedSubtitle.value diff --git a/client/src/types/graphql.tsx b/client/src/types/graphql.tsx index 433f11781..9660821d2 100644 --- a/client/src/types/graphql.tsx +++ b/client/src/types/graphql.tsx @@ -3707,7 +3707,7 @@ export type GetSystemObjectDetailsQuery = ( { __typename?: 'Query' } & { getSystemObjectDetails: ( { __typename?: 'GetSystemObjectDetailsResult' } - & Pick + & Pick & { identifiers: Array<( { __typename?: 'IngestIdentifier' } & Pick @@ -6561,6 +6561,7 @@ export const GetSystemObjectDetailsDocument = gql` idSystemObject idObject name + subTitle retired objectType allowed diff --git a/server/graphql/api/queries/systemobject/getSystemObjectDetails.ts b/server/graphql/api/queries/systemobject/getSystemObjectDetails.ts index 7c48f662a..3bb43f195 100644 --- a/server/graphql/api/queries/systemobject/getSystemObjectDetails.ts +++ b/server/graphql/api/queries/systemobject/getSystemObjectDetails.ts @@ -6,6 +6,7 @@ const getSystemObjectDetails = gql` idSystemObject idObject name + subTitle retired objectType allowed From 32d4e3594983e692f9ffd8bb929889cdad496a60 Mon Sep 17 00:00:00 2001 From: Hsin Tung Date: Sat, 26 Mar 2022 02:34:18 -0700 Subject: [PATCH 11/31] clean up and address lint --- client/src/store/repository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/store/repository.ts b/client/src/store/repository.ts index c30382b2c..71a837468 100644 --- a/client/src/store/repository.ts +++ b/client/src/store/repository.ts @@ -456,7 +456,7 @@ export const useRepositoryStore = create((set: SetState Date: Sat, 26 Mar 2022 02:34:44 -0700 Subject: [PATCH 12/31] clean up and address lint pt 2 --- client/src/components/controls/InputField.tsx | 2 +- .../Metadata/Control/SubtitleControl.tsx | 20 +++--- .../components/Metadata/Model/index.tsx | 24 +++---- .../Metadata/Scene/SceneDataForm.tsx | 1 - .../components/Metadata/Scene/index.tsx | 18 ++--- .../components/SubjectItem/ItemList.tsx | 66 ++++++++++++------- .../components/SubjectItem/index.tsx | 2 +- .../DetailsView/DetailsTab/ItemDetails.tsx | 2 +- .../DetailsView/DetailsTab/ModelDetails.tsx | 1 - .../DetailsView/DetailsTab/SubjectDetails.tsx | 2 +- .../components/DetailsView/index.tsx | 2 +- client/src/store/item.ts | 42 ++++++------ client/src/store/metadata/index.ts | 10 +-- .../src/store/metadata/metadata.defaults.ts | 7 +- client/src/store/metadata/metadata.types.ts | 10 +-- client/src/store/repository.ts | 1 - client/src/store/utils.ts | 4 +- server/graphql/api/index.ts | 2 +- server/utils/nameHelpers.ts | 4 +- 19 files changed, 118 insertions(+), 102 deletions(-) diff --git a/client/src/components/controls/InputField.tsx b/client/src/components/controls/InputField.tsx index 7472e0c51..300d7c1ee 100644 --- a/client/src/components/controls/InputField.tsx +++ b/client/src/components/controls/InputField.tsx @@ -41,7 +41,7 @@ interface InputFieldProps extends ViewableProps { padding?: string; inputHeight?: string; gridGap?: string; - containerStyle?: any; + containerStyle?: React.CSSProperties; } function InputField(props: InputFieldProps): React.ReactElement { diff --git a/client/src/pages/Ingestion/components/Metadata/Control/SubtitleControl.tsx b/client/src/pages/Ingestion/components/Metadata/Control/SubtitleControl.tsx index c0175fa45..10fa17b4d 100644 --- a/client/src/pages/Ingestion/components/Metadata/Control/SubtitleControl.tsx +++ b/client/src/pages/Ingestion/components/Metadata/Control/SubtitleControl.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { SubtitleFields, eSubtitleOption } from '../../../../../store/metadata/metadata.types'; -import { Box, makeStyles, Typography, Table, TableBody, TableCell, TableContainer, TableRow, fade } from '@material-ui/core' +import { Box, makeStyles, Typography, Table, TableBody, TableCell, TableContainer, TableRow, fade } from '@material-ui/core'; import { RiCheckboxBlankCircleLine, RiRecordCircleFill } from 'react-icons/ri'; import { grey } from '@material-ui/core/colors'; import { palette } from '../../../../../theme'; @@ -8,7 +8,7 @@ import { DebounceInput } from 'react-debounce-input'; import clsx from 'clsx'; interface SubtitleControlProps { - subtitles: SubtitleFields; + subtitles: SubtitleFields; objectName: string; onSelectSubtitle: (id: number) => void; onUpdateCustomSubtitle: (event: React.ChangeEvent, id: number) => void; @@ -50,7 +50,7 @@ const useStyles = makeStyles(({ palette, typography }) => ({ text: { fontSize: '0.75rem' } -})) +})); function SubtitleControl(props: SubtitleControlProps): React.ReactElement { const { objectName, subtitles, onUpdateCustomSubtitle, onSelectSubtitle, hasPrimaryTheme } = props; @@ -118,10 +118,12 @@ function SubtitleControl(props: SubtitleControlProps): React.ReactElement {
{!selected && onSelectSubtitle(id)} size={18} color={grey[400]} />} {selected && onSelectSubtitle(id)} size={18} color={palette.primary.main} />} - { - subtitleOption === eSubtitleOption.eInherit ? {value} - : subtitleOption === eSubtitleOption.eNone ? None - : {value} + ) : subtitleOption === eSubtitleOption.eNone ? ( + None + ) : ( + onUpdateCustomSubtitle(e, id)} element='input' value={value} @@ -129,7 +131,7 @@ function SubtitleControl(props: SubtitleControlProps): React.ReactElement { debounceTimeout={400} title={`subtitle-input-${value}`} /> - } + )}
)) } @@ -140,7 +142,7 @@ function SubtitleControl(props: SubtitleControlProps): React.ReactElement { ); return {options}; - } + }; return ( diff --git a/client/src/pages/Ingestion/components/Metadata/Model/index.tsx b/client/src/pages/Ingestion/components/Metadata/Model/index.tsx index 58015ac77..53a72b304 100644 --- a/client/src/pages/Ingestion/components/Metadata/Model/index.tsx +++ b/client/src/pages/Ingestion/components/Metadata/Model/index.tsx @@ -276,7 +276,7 @@ function Model(props: ModelProps): React.ReactElement { value: subtitle.value, subtitleOption: subtitle.subtitleOption, selected: id === subtitle.id - } + }; }); updateMetadataField(metadataIndex, 'subtitles', updatedSubtitles, MetadataType.model); // console.log('id', id, updatedSubtitles); @@ -346,17 +346,17 @@ function Model(props: ModelProps): React.ReactElement { Model - {!idAsset && ( - - - - )} + {!idAsset && ( + + + + )} diff --git a/client/src/pages/Ingestion/components/Metadata/Scene/SceneDataForm.tsx b/client/src/pages/Ingestion/components/Metadata/Scene/SceneDataForm.tsx index 1cd331c8c..521cb0cb6 100644 --- a/client/src/pages/Ingestion/components/Metadata/Scene/SceneDataForm.tsx +++ b/client/src/pages/Ingestion/components/Metadata/Scene/SceneDataForm.tsx @@ -88,7 +88,6 @@ function SceneDataForm(props: SceneDataProps): React.ReactElement {
- diff --git a/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx b/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx index 0c08f6d70..ae79830ad 100644 --- a/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx +++ b/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx @@ -181,7 +181,7 @@ function Scene(props: SceneProps): React.ReactElement { const updatedSourceObjects = sourceObjects.filter(sourceObject => sourceObject.idSystemObject !== idSystemObject); updateMetadataField(metadataIndex, 'sourceObjects', updatedSourceObjects, MetadataType.scene); - const { data: { getIngestTitle: { ingestTitle }}}: ApolloQueryResult = await apolloClient.query({ + const { data: { getIngestTitle: { ingestTitle } } }: ApolloQueryResult = await apolloClient.query({ query: GetIngestTitleDocument, variables: { input: { @@ -190,10 +190,10 @@ function Scene(props: SceneProps): React.ReactElement { }, fetchPolicy: 'no-cache' }); - + if (!ingestTitle) { toast.error('Failed to fetch titles for ingestion items'); - return + return; } const subtitleState = parseSubtitlesToState(ingestTitle); updateMetadataField(metadataIndex, 'subtitles', subtitleState, MetadataType.scene); @@ -215,9 +215,9 @@ function Scene(props: SceneProps): React.ReactElement { const onSelectedObjects = async (newSourceObjects: StateRelatedObject[]) => { updateMetadataField(metadataIndex, objectRelationship === RelatedObjectType.Source ? 'sourceObjects' : 'derivedObjects', newSourceObjects, MetadataType.scene); - + if (objectRelationship === RelatedObjectType.Source) { - const { data: { getIngestTitle: { ingestTitle }}}: ApolloQueryResult = await apolloClient.query({ + const { data: { getIngestTitle: { ingestTitle } } }: ApolloQueryResult = await apolloClient.query({ query: GetIngestTitleDocument, variables: { input: { @@ -226,15 +226,15 @@ function Scene(props: SceneProps): React.ReactElement { }, fetchPolicy: 'no-cache' }); - + if (!ingestTitle) { toast.error('Failed to fetch titles for ingestion items'); - return + return; } const subtitleState = parseSubtitlesToState(ingestTitle); updateMetadataField(metadataIndex, 'subtitles', subtitleState, MetadataType.scene); updateMetadataField(metadataIndex, 'name', ingestTitle.title, MetadataType.scene); - } + } onModalClose(); }; @@ -246,7 +246,7 @@ function Scene(props: SceneProps): React.ReactElement { value: subtitle.value, subtitleOption: subtitle.subtitleOption, selected: id === subtitle.id - } + }; }); updateMetadataField(metadataIndex, 'subtitles', updatedSubtitles, MetadataType.scene); }; diff --git a/client/src/pages/Ingestion/components/SubjectItem/ItemList.tsx b/client/src/pages/Ingestion/components/SubjectItem/ItemList.tsx index 7d4bb08b6..a826225f6 100644 --- a/client/src/pages/Ingestion/components/SubjectItem/ItemList.tsx +++ b/client/src/pages/Ingestion/components/SubjectItem/ItemList.tsx @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + /** * ItemList * @@ -113,7 +115,19 @@ function ItemList(): React.ReactElement { {(items && items.length > 0) && items.map(getItemsList)} - {hasNewItem ? 1} /> : } + {hasNewItem ? ( + 1} + /> + ) : ( + + )}
@@ -177,7 +191,7 @@ function ItemListEmptyItem(props: ItemListEmptyItemProps) { Add new media group here - + ); @@ -222,30 +236,34 @@ function ItemListNewItem(props: ItemListNewItemProps) { - {hasMultipleSubjects ? No : } - + {hasMultipleSubjects ? ( + No + ) : ( + + )} - {(idProject > -1) && ()} - + {(idProject > -1) && ( + + )} ); diff --git a/client/src/pages/Ingestion/components/SubjectItem/index.tsx b/client/src/pages/Ingestion/components/SubjectItem/index.tsx index b81e63849..879505ddc 100644 --- a/client/src/pages/Ingestion/components/SubjectItem/index.tsx +++ b/client/src/pages/Ingestion/components/SubjectItem/index.tsx @@ -75,7 +75,7 @@ function SubjectItem(): React.ReactElement { toast.dismiss(); let error: boolean = false; // Note: we only want certain warnings to flag if we have missing fields after selecting an new item - let isItemSelected = !!selectedItem; + const isItemSelected = !!selectedItem; if (!subjects.length) { error = true; diff --git a/client/src/pages/Repository/components/DetailsView/DetailsTab/ItemDetails.tsx b/client/src/pages/Repository/components/DetailsView/DetailsTab/ItemDetails.tsx index 8da1ae806..7dd110f30 100644 --- a/client/src/pages/Repository/components/DetailsView/DetailsTab/ItemDetails.tsx +++ b/client/src/pages/Repository/components/DetailsView/DetailsTab/ItemDetails.tsx @@ -20,7 +20,7 @@ export interface ItemDetailFields extends SubjectDetailFields { function ItemDetails(props: DetailComponentProps): React.ReactElement { const { data, loading, disabled, onUpdateDetail, objectType, subtitle, onSubtitleUpdate } = props; const [ItemDetails, updateDetailField] = useDetailTabStore(state => [state.ItemDetails, state.updateDetailField]); - + useEffect(() => { onUpdateDetail(objectType, ItemDetails); }, [ItemDetails]); diff --git a/client/src/pages/Repository/components/DetailsView/DetailsTab/ModelDetails.tsx b/client/src/pages/Repository/components/DetailsView/DetailsTab/ModelDetails.tsx index bdd48da6c..2c0c1e21e 100644 --- a/client/src/pages/Repository/components/DetailsView/DetailsTab/ModelDetails.tsx +++ b/client/src/pages/Repository/components/DetailsView/DetailsTab/ModelDetails.tsx @@ -101,7 +101,6 @@ export const useStyles = makeStyles(({ palette, typography }) => ({ fontSize: '0.8em', height: 3 } - })); function ModelDetails(props: DetailComponentProps): React.ReactElement { diff --git a/client/src/pages/Repository/components/DetailsView/DetailsTab/SubjectDetails.tsx b/client/src/pages/Repository/components/DetailsView/DetailsTab/SubjectDetails.tsx index 635b84726..3625301be 100644 --- a/client/src/pages/Repository/components/DetailsView/DetailsTab/SubjectDetails.tsx +++ b/client/src/pages/Repository/components/DetailsView/DetailsTab/SubjectDetails.tsx @@ -105,7 +105,7 @@ export function SubjectFields(props: SubjectFieldsProps): React.ReactElement { Subtitle - ({ ...details, subtitle: target.value })); updatedDataFields.Subtitle = target.value; setUpdatedData(updatedDataFields); - } + }; const onUpdateDetail = (objectType: number, data: UpdateDataFields): void => { // console.log('onUpdateDetail', objectType, data); diff --git a/client/src/store/item.ts b/client/src/store/item.ts index 10b46813d..4e2aea27d 100644 --- a/client/src/store/item.ts +++ b/client/src/store/item.ts @@ -24,7 +24,7 @@ export type StateItem = { selected: boolean; idProject: number; projectName: string; -} +}; export type StateProject = { id: number; @@ -74,23 +74,23 @@ export const useItemStore = create((set: SetState, get: Ge const { items } = get(); const currentDefaultItem = lodash.find(items, { id: defaultItem.id }); - if (!fetchedItems.length) - return; - - const newItemSelected = lodash.find(fetchedItems, { selected: true }); + if (!fetchedItems.length) + return; + + const newItemSelected = lodash.find(fetchedItems, { selected: true }); - if (!newItemSelected) - fetchedItems[0].selected = true; + if (!newItemSelected) + fetchedItems[0].selected = true; - if (currentDefaultItem) - currentDefaultItem.selected = false; - - const newItems: StateItem[] = [...fetchedItems]; - if (currentDefaultItem) - newItems.push(currentDefaultItem); + if (currentDefaultItem) + currentDefaultItem.selected = false; + const newItems: StateItem[] = [...fetchedItems]; + if (currentDefaultItem) + newItems.push(currentDefaultItem); - set({ items: newItems, loading: false }); + + set({ items: newItems, loading: false }); }, addNewItem: async () => { const { items, newItem } = get(); @@ -117,17 +117,17 @@ export const useItemStore = create((set: SetState, get: Ge newItemCopy.selected = true; set({ hasNewItem: true, items: itemsCopy, newItem: newItemCopy }); }, - + updateNewItemEntireSubject: (entireSubject: boolean) => { const { newItem } = get(); const newItemCopy = lodash.cloneDeep(newItem); - set({ newItem: { ...newItemCopy, entireSubject }}); + set({ newItem: { ...newItemCopy, entireSubject } }); }, updateNewItemSubtitle: (event: React.ChangeEvent) => { const { newItem } = get(); const { target: { value: subtitle } } = event; - set({ newItem: { ...newItem, subtitle }}) + set({ newItem: { ...newItem, subtitle } }); }, updateNewItemProject: (idProject: number) => { const { newItem, projectList } = get(); @@ -152,9 +152,9 @@ export const useItemStore = create((set: SetState, get: Ge return { ...item, selected: id === item.id ? !item.selected : false - } - }) - set({ newItem, items: updatedItems }) + }; + }); + set({ newItem, items: updatedItems }); }, loadingItems: (): void => { set({ loading: true }); @@ -170,7 +170,7 @@ export const useItemStore = create((set: SetState, get: Ge }, fetchPolicy: 'no-cache' }); - const { data: { getIngestionItems: { IngestionItem }} } = ingestionItemQuery; + const { data: { getIngestionItems: { IngestionItem } } } = ingestionItemQuery; const ingestionItemState = IngestionItem?.map(item => parseIngestionItemToState(item)); set({ items: ingestionItemState }); } catch (error) { diff --git a/client/src/store/metadata/index.ts b/client/src/store/metadata/index.ts index 86444d6e9..3ecabe9e2 100644 --- a/client/src/store/metadata/index.ts +++ b/client/src/store/metadata/index.ts @@ -413,7 +413,7 @@ export const useMetadataStore = create((set: SetState = await apolloClient.query({ + const { data: { getIngestTitle: { ingestTitle } } }: ApolloQueryResult = await apolloClient.query({ query: GetIngestTitleDocument, variables: { input: { @@ -425,10 +425,10 @@ export const useMetadataStore = create((set: SetState((set: SetState { const { getInitialEntry, getEntries } = useVocabularyStore.getState(); diff --git a/client/src/store/metadata/metadata.defaults.ts b/client/src/store/metadata/metadata.defaults.ts index 4a1e567de..1bd2fbd34 100644 --- a/client/src/store/metadata/metadata.defaults.ts +++ b/client/src/store/metadata/metadata.defaults.ts @@ -32,7 +32,7 @@ const subtitleSchema = yup.object().shape({ selected: yup.boolean().required(), subtitleOption: yup.number().required(), id: yup.number() -}) +}); const identifierValidation = { test: array => array.length && array.every(identifier => identifier.identifier.length), @@ -47,7 +47,7 @@ const identifiersWhenValidation = { const hasModelSourcesValidation = { test: array => array.length && array.some(source => source.objectType === eSystemObjectType.eModel), message: 'Should provide at least 1 model parent for scene ingestion' -} +}; const notesWhenUpdate = { is: value => value > 0, @@ -59,8 +59,7 @@ const selectedSubtitleValidation = { const selectedSubtitle = array.find(subtitle => subtitle.selected); if (!selectedSubtitle) return false; if (selectedSubtitle.subtitleOption !== eSubtitleOption.eNone) - return !!selectedSubtitle.value - + return !!selectedSubtitle.value; return true; }, message: 'Should provide a valid subtitle/name for ingestion' diff --git a/client/src/store/metadata/metadata.types.ts b/client/src/store/metadata/metadata.types.ts index 3cf679d6f..19df9377d 100644 --- a/client/src/store/metadata/metadata.types.ts +++ b/client/src/store/metadata/metadata.types.ts @@ -31,11 +31,11 @@ export enum eSubtitleOption { } export type SubtitleFields = { - value: string, - selected: boolean, - subtitleOption: eSubtitleOption, - id: number - }[] + value: string, + selected: boolean, + subtitleOption: eSubtitleOption, + id: number +}[]; export type MetadataInfo = { metadata: StateMetadata; diff --git a/client/src/store/repository.ts b/client/src/store/repository.ts index 71a837468..c06c42da2 100644 --- a/client/src/store/repository.ts +++ b/client/src/store/repository.ts @@ -453,7 +453,6 @@ export const useRepositoryStore = create((set: SetState => { - console.log('idSystemObject', idSystemObject); const { getFilterState } = get(); const filter = getFilterState(); const { data, error } = await getObjectChildrenForRoot(filter, idSystemObject); diff --git a/client/src/store/utils.ts b/client/src/store/utils.ts index 5b3a3f0e3..4c083ece3 100644 --- a/client/src/store/utils.ts +++ b/client/src/store/utils.ts @@ -54,7 +54,7 @@ export function parseIngestionItemToState(ingestionItem: IngestionItem): StateIt selected: false, idProject, projectName: ProjectName - } + }; } export function parseProjectToState(project: Project, selected: boolean): StateProject { @@ -121,7 +121,7 @@ export function parseSubtitlesToState(titles: IngestTitle): SubtitleFields { const result: SubtitleFields = []; // If forced, user is required to use the value from subtitle if (forced && subtitle) { - result.push({ value: subtitle[0] as string, selected: true, subtitleOption: eSubtitleOption.eForced, id: 0 }) + result.push({ value: subtitle[0] as string, selected: true, subtitleOption: eSubtitleOption.eForced, id: 0 }); return result; } diff --git a/server/graphql/api/index.ts b/server/graphql/api/index.ts index a555a017a..c5c8fef29 100644 --- a/server/graphql/api/index.ts +++ b/server/graphql/api/index.ts @@ -764,7 +764,7 @@ class GraphQLApi { operationName, variables, context - }) + }); } private async graphqlRequest({ query, variables, context, operationName }: GraphQLRequest): Promise { diff --git a/server/utils/nameHelpers.ts b/server/utils/nameHelpers.ts index ebf97a85b..727fb6ec3 100644 --- a/server/utils/nameHelpers.ts +++ b/server/utils/nameHelpers.ts @@ -157,8 +157,8 @@ export class NameHelpers { const mergedSubtitle: string = [...subtitleSet].join(', '); if (mergedSubtitle) subtitle.push(mergedSubtitle); - subtitle.push(''); - subtitle.push(null); + subtitle.push(''); + subtitle.push(null); } else subtitle.push(null); return { title: subject?.Name ?? null, subtitle }; From f7ab60d98682dd83fe9ee77baf1653176837cfa8 Mon Sep 17 00:00:00 2001 From: Jon Tyson <6943745+jahjedtieson@users.noreply.github.com> Date: Sat, 26 Mar 2022 11:04:53 -0700 Subject: [PATCH 13/31] GraphQL: * Added getIngestionItems to master allQueries object --- server/graphql/api/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/graphql/api/index.ts b/server/graphql/api/index.ts index c5c8fef29..1934c2e0c 100644 --- a/server/graphql/api/index.ts +++ b/server/graphql/api/index.ts @@ -151,6 +151,7 @@ import getFilterViewData from './queries/repository/getFilterViewData'; import getAllUsers from './queries/user/getAllUsers'; import getUnitsFromNameSearch from './queries/unit/getUnitsFromNameSearch'; import getProjectList from './queries/systemobject/getProjectList'; +import getIngestionItems from './queries/unit/getIngestionItems'; // Mutations import createUser from './mutations/user/createUser'; @@ -221,7 +222,8 @@ const allQueries = { getAllUsers, updateUser, getUnitsFromNameSearch, - getProjectList + getProjectList, + getIngestionItems }; type GraphQLRequest = { From 217b17c73518d8457fcad39f484782ef55b156ff Mon Sep 17 00:00:00 2001 From: Jon Tyson <6943745+jahjedtieson@users.noreply.github.com> Date: Sat, 26 Mar 2022 16:44:41 -0700 Subject: [PATCH 14/31] System: * Address issue in NameHelpers.modelTitleOptions, where we failed to provide the user with an option to enter the title if the owning item had an empty title --- server/utils/nameHelpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/utils/nameHelpers.ts b/server/utils/nameHelpers.ts index 727fb6ec3..d62f9cd4e 100644 --- a/server/utils/nameHelpers.ts +++ b/server/utils/nameHelpers.ts @@ -51,7 +51,7 @@ export class NameHelpers { const title: string = (item.Title) ? item.Name.replace(`: ${item.Title}`, '') : item.Name; // base title is the item's display name, with its subtitle removed, if any const subtitle: (string | null)[] = []; subtitle.push(item.Title); // user can select the default item subtitle. - if (item.Title) // if we record an entry with a real subtitle, + if (item.Title !== null) // if we record an entry with a real or empty subtitle, subtitle.push(null); // provide an entry with null subtitle, indicating the user can enter one return { title, forced: false, subtitle }; } From 54c2e540b51170e29c633010492fc1eceb434f83 Mon Sep 17 00:00:00 2001 From: Jon Tyson <6943745+jahjedtieson@users.noreply.github.com> Date: Sun, 27 Mar 2022 19:51:23 -0700 Subject: [PATCH 15/31] GraphQL: * Fix item, model, and scene name updates in updateObjectDetails, by handling a missing subtitle properly --- .../resolvers/mutations/updateObjectDetails.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/server/graphql/schema/systemobject/resolvers/mutations/updateObjectDetails.ts b/server/graphql/schema/systemobject/resolvers/mutations/updateObjectDetails.ts index f1e8dc875..6f70854be 100644 --- a/server/graphql/schema/systemobject/resolvers/mutations/updateObjectDetails.ts +++ b/server/graphql/schema/systemobject/resolvers/mutations/updateObjectDetails.ts @@ -181,7 +181,8 @@ export default async function updateObjectDetails(_: Parent, args: MutationUpdat if (!Item) return sendResult(false, `Unable to fetch Media Group with id ${idObject}; update failed`); - Item.Name = (data.Name && !data.Subtitle) ? data.Name : computeNewName(Item.Name, Item.Title, data.Subtitle); // do this before updating .Title + const namedWithoutSubtitle: boolean = (data.Name != null && data.Subtitle == null); + Item.Name = namedWithoutSubtitle ? data.Name : computeNewName(Item.Name, Item.Title, data.Subtitle); // do this before updating .Title Item.Title = data.Subtitle ?? null; if (!isNull(EntireSubject) && !isUndefined(EntireSubject)) @@ -323,7 +324,8 @@ export default async function updateObjectDetails(_: Parent, args: MutationUpdat ModelFileType } = data.Model; - Model.Name = (data.Name && !data.Subtitle) ? data.Name : computeNewName(Model.Name, Model.Title, data.Subtitle); // do this before updating .Title + const namedWithoutSubtitle: boolean = (data.Name != null && data.Subtitle == null); + Model.Name = namedWithoutSubtitle ? data.Name : computeNewName(Model.Name, Model.Title, data.Subtitle); // do this before updating .Title Model.Title = data.Subtitle ?? null; if (CreationMethod) Model.idVCreationMethod = CreationMethod; @@ -345,7 +347,8 @@ export default async function updateObjectDetails(_: Parent, args: MutationUpdat const oldPosedAndQCd: boolean = Scene.PosedAndQCd; - Scene.Name = (data.Name && !data.Subtitle) ? data.Name : computeNewName(Scene.Name, Scene.Title, data.Subtitle); // do this before updated .Title + const namedWithoutSubtitle: boolean = (data.Name != null && data.Subtitle == null); + Scene.Name = namedWithoutSubtitle ? data.Name : computeNewName(Scene.Name, Scene.Title, data.Subtitle); // do this before updated .Title Scene.Title = data.Subtitle ?? null; if (data.Scene) { From 29087eecb9a45c9d544b3f268b9095ff4f7a6019 Mon Sep 17 00:00:00 2001 From: Hsin Tung Date: Sun, 27 Mar 2022 20:19:58 -0700 Subject: [PATCH 16/31] address missing MG name, lack of empty subtitle, and undesired automatic model name conversion --- .../components/Metadata/Control/SubtitleControl.tsx | 2 +- .../src/pages/Ingestion/components/Metadata/Model/index.tsx | 4 ---- .../src/pages/Ingestion/components/Metadata/Scene/index.tsx | 4 +++- client/src/store/metadata/index.ts | 1 - client/src/store/utils.ts | 5 ++--- .../schema/unit/resolvers/queries/getIngestionItems.ts | 2 +- 6 files changed, 7 insertions(+), 11 deletions(-) diff --git a/client/src/pages/Ingestion/components/Metadata/Control/SubtitleControl.tsx b/client/src/pages/Ingestion/components/Metadata/Control/SubtitleControl.tsx index 10fa17b4d..838c55c45 100644 --- a/client/src/pages/Ingestion/components/Metadata/Control/SubtitleControl.tsx +++ b/client/src/pages/Ingestion/components/Metadata/Control/SubtitleControl.tsx @@ -120,7 +120,7 @@ function SubtitleControl(props: SubtitleControlProps): React.ReactElement { {selected && onSelectSubtitle(id)} size={18} color={palette.primary.main} />} {subtitleOption === eSubtitleOption.eInherit ? ( {value} - ) : subtitleOption === eSubtitleOption.eNone ? ( + ) : (subtitleOption === eSubtitleOption.eNone || value === '') ? ( None ) : ( , id: number) => { @@ -293,7 +290,6 @@ function Model(props: ModelProps): React.ReactElement { targetSubtitle.value = event.target.value; updateMetadataField(metadataIndex, 'subtitles', subtitlesCopy, MetadataType.model); - // console.log('event', event.target.value, id); }; return ( diff --git a/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx b/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx index ae79830ad..6d7ec02b9 100644 --- a/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx +++ b/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx @@ -308,6 +308,9 @@ function Scene(props: SceneProps): React.ReactElement { />
+ + + - )} ((set: SetState, name: string, value: MetadataFieldValue, metadataType: MetadataType) => { const { metadatas } = get(); - if (!(name in metadatas[metadataIndex][metadataType])) { toast.error(`Field ${name} doesn't exist on a ${metadataType} asset`); return; diff --git a/client/src/store/utils.ts b/client/src/store/utils.ts index 4c083ece3..177b672b6 100644 --- a/client/src/store/utils.ts +++ b/client/src/store/utils.ts @@ -32,7 +32,6 @@ export function isNewItem(id: string): boolean { export function parseItemToState(item: Item, selected: boolean, position: number): StateItem { const { idItem, Name, EntireSubject } = item; const id = idItem || `${position}-new-item`; - console.log('item', item); return { id: String(id), entireSubject: EntireSubject, @@ -46,7 +45,6 @@ export function parseItemToState(item: Item, selected: boolean, position: number export function parseIngestionItemToState(ingestionItem: IngestionItem): StateItem { const { idItem, EntireSubject, MediaGroupName, idProject, ProjectName } = ingestionItem; - console.log('ingestionItem', ingestionItem); return { id: String(idItem), subtitle: MediaGroupName, @@ -136,9 +134,10 @@ export function parseSubtitlesToState(titles: IngestTitle): SubtitleFields { result.push({ value: '', selected: true, subtitleOption: eSubtitleOption.eInput, id: key }); } // Inherited Value - if (subtitleVal && subtitleVal !== '' && subtitleVal.length) { + if (subtitleVal && subtitleVal !== '') { result.push({ value: subtitleVal, selected: false, subtitleOption: eSubtitleOption.eInherit, id: key }); } + }); } diff --git a/server/graphql/schema/unit/resolvers/queries/getIngestionItems.ts b/server/graphql/schema/unit/resolvers/queries/getIngestionItems.ts index 1083d9c66..7c04a0966 100644 --- a/server/graphql/schema/unit/resolvers/queries/getIngestionItems.ts +++ b/server/graphql/schema/unit/resolvers/queries/getIngestionItems.ts @@ -31,7 +31,7 @@ export default async function getIngestionItems(_: Parent, args: QueryGetIngesti IngestionItem.push({ idItem: itemAndProject.idItem, EntireSubject: itemAndProject.EntireSubject ?? true, - MediaGroupName: itemAndProject.Title ?? itemAndProject.Name ?? '', + MediaGroupName: itemAndProject.Name ?? '', idProject: itemAndProject.idProject, ProjectName: itemAndProject.ProjectName ?? 'Unknown', }); From be703412eee49e1d3cf476553bb03b1ee1314350 Mon Sep 17 00:00:00 2001 From: Hsin Tung Date: Sun, 27 Mar 2022 21:13:42 -0700 Subject: [PATCH 17/31] remove name validation for model ingestion and addressed bug that misreads an input subtitle option --- .../components/Metadata/Control/SubtitleControl.tsx | 10 ++++------ client/src/store/metadata/metadata.defaults.ts | 1 - 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/client/src/pages/Ingestion/components/Metadata/Control/SubtitleControl.tsx b/client/src/pages/Ingestion/components/Metadata/Control/SubtitleControl.tsx index 838c55c45..8203a0a11 100644 --- a/client/src/pages/Ingestion/components/Metadata/Control/SubtitleControl.tsx +++ b/client/src/pages/Ingestion/components/Metadata/Control/SubtitleControl.tsx @@ -117,12 +117,10 @@ function SubtitleControl(props: SubtitleControlProps): React.ReactElement { sortedSubtitles.map(({ selected, value, id, subtitleOption }, key) => (
{!selected && onSelectSubtitle(id)} size={18} color={grey[400]} />} - {selected && onSelectSubtitle(id)} size={18} color={palette.primary.main} />} - {subtitleOption === eSubtitleOption.eInherit ? ( - {value} - ) : (subtitleOption === eSubtitleOption.eNone || value === '') ? ( - None - ) : ( + {selected && onSelectSubtitle(id)} size={18} color={palette.primary.main} />} + {subtitleOption === eSubtitleOption.eNone && None} + {subtitleOption === eSubtitleOption.eInherit && {value.length ? value : 'None'}} + {subtitleOption === eSubtitleOption.eInput && ( onUpdateCustomSubtitle(e, id)} element='input' diff --git a/client/src/store/metadata/metadata.defaults.ts b/client/src/store/metadata/metadata.defaults.ts index 1bd2fbd34..b085b210f 100644 --- a/client/src/store/metadata/metadata.defaults.ts +++ b/client/src/store/metadata/metadata.defaults.ts @@ -177,7 +177,6 @@ export const defaultModelFields: ModelFields = { }; export const modelFieldsSchemaUpdate = yup.object().shape({ - name: yup.string().min(1, 'Name must have at least one character').required('Name is required'), systemCreated: yup.boolean().required(), uvMaps: yup.array().of(uvMapSchema), sourceObjects: yup.array().of(sourceObjectSchema), From b616c04bfa040ce29d2666734ce58a37f6a64d11 Mon Sep 17 00:00:00 2001 From: Hsin Tung Date: Sun, 27 Mar 2022 21:15:54 -0700 Subject: [PATCH 18/31] address lint --- .../Ingestion/components/Metadata/Control/SubtitleControl.tsx | 2 +- client/src/pages/Ingestion/components/Metadata/Scene/index.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/client/src/pages/Ingestion/components/Metadata/Control/SubtitleControl.tsx b/client/src/pages/Ingestion/components/Metadata/Control/SubtitleControl.tsx index 8203a0a11..772642df8 100644 --- a/client/src/pages/Ingestion/components/Metadata/Control/SubtitleControl.tsx +++ b/client/src/pages/Ingestion/components/Metadata/Control/SubtitleControl.tsx @@ -117,7 +117,7 @@ function SubtitleControl(props: SubtitleControlProps): React.ReactElement { sortedSubtitles.map(({ selected, value, id, subtitleOption }, key) => (
{!selected && onSelectSubtitle(id)} size={18} color={grey[400]} />} - {selected && onSelectSubtitle(id)} size={18} color={palette.primary.main} />} + {selected && onSelectSubtitle(id)} size={18} color={palette.primary.main} />} {subtitleOption === eSubtitleOption.eNone && None} {subtitleOption === eSubtitleOption.eInherit && {value.length ? value : 'None'}} {subtitleOption === eSubtitleOption.eInput && ( diff --git a/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx b/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx index 6d7ec02b9..298a4ed06 100644 --- a/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx +++ b/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx @@ -308,7 +308,6 @@ function Scene(props: SceneProps): React.ReactElement { /> - From 3a72234ae93b14bf67cfe24729a85edc0af4dd26 Mon Sep 17 00:00:00 2001 From: Hsin Tung Date: Sun, 27 Mar 2022 23:26:13 -0700 Subject: [PATCH 19/31] fix breadcrumb name for MG during ingestion --- client/src/pages/Ingestion/components/Metadata/index.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/client/src/pages/Ingestion/components/Metadata/index.tsx b/client/src/pages/Ingestion/components/Metadata/index.tsx index 82f91f50c..4db1e34fa 100644 --- a/client/src/pages/Ingestion/components/Metadata/index.tsx +++ b/client/src/pages/Ingestion/components/Metadata/index.tsx @@ -29,7 +29,8 @@ import { useItemStore, useMetadataStore, useVocabularyStore, - useUploadStore + useUploadStore, + useSubjectStore } from '../../../../store'; import useIngest from '../../hooks/useIngest'; import Model from './Model'; @@ -237,8 +238,8 @@ interface BreadcrumbsHeaderProps { function BreadcrumbsHeader(props: BreadcrumbsHeaderProps) { const classes = useStyles(); + const [subjects] = useSubjectStore(state => [state.subjects]); const { projectName, item, metadata, customBreadcrumbs, customBreadcrumbsArr } = props; - let content: React.ReactNode; if (customBreadcrumbs && customBreadcrumbsArr?.length) { @@ -256,8 +257,8 @@ function BreadcrumbsHeader(props: BreadcrumbsHeaderProps) { content = ( }> Specify metadata for: Project: {projectName} - Media Group: {item?.subtitle} - {metadata.file.name} + Media Group: {subjects.length > 1 ? item?.subtitle : item?.id === 'default' ? `${subjects?.[0]?.name}${item?.subtitle ? `: ${item?.subtitle}` : ''}` : subjects?.[0]?.name} + {metadata?.file?.name} ); } From 41cf06773fa1a88f3e9ae452b40f56221d5d47ff Mon Sep 17 00:00:00 2001 From: Hsin Tung Date: Mon, 28 Mar 2022 12:30:37 -0700 Subject: [PATCH 20/31] test model and scene ingestion subtitle control and revise UI for ItemList in subject/item step --- .../Ingestion/components/Metadata/Scene/index.tsx | 4 ++-- .../Ingestion/components/SubjectItem/ItemList.tsx | 13 +++++-------- client/src/store/metadata/metadata.defaults.ts | 2 +- client/src/store/utils.ts | 2 +- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx b/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx index 298a4ed06..2452f3fd9 100644 --- a/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx +++ b/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx @@ -185,7 +185,7 @@ function Scene(props: SceneProps): React.ReactElement { query: GetIngestTitleDocument, variables: { input: { - sourceObjects: scene.sourceObjects + sourceObjects: updatedSourceObjects } }, fetchPolicy: 'no-cache' @@ -221,7 +221,7 @@ function Scene(props: SceneProps): React.ReactElement { query: GetIngestTitleDocument, variables: { input: { - sourceObjects: scene.sourceObjects + sourceObjects: newSourceObjects } }, fetchPolicy: 'no-cache' diff --git a/client/src/pages/Ingestion/components/SubjectItem/ItemList.tsx b/client/src/pages/Ingestion/components/SubjectItem/ItemList.tsx index a826225f6..e1e6161f9 100644 --- a/client/src/pages/Ingestion/components/SubjectItem/ItemList.tsx +++ b/client/src/pages/Ingestion/components/SubjectItem/ItemList.tsx @@ -78,11 +78,8 @@ const useStyles = makeStyles(({ palette, spacing, typography, breakpoints }) => function ItemList(): React.ReactElement { const classes = useStyles(); - const [items, hasNewItem, newItem, addNewItem, projectList, updateNewItemSubtitle, updateNewItemEntireSubject, updateNewItemProject, updateSelectedItem] = useItemStore(state => [state.items, /*state.updateItem,*/ state.hasNewItem, state.newItem, state.addNewItem, state.projectList, state.updateNewItemSubtitle, state.updateNewItemEntireSubject, state.updateNewItemProject, state.updateSelectedItem]); + const [items, hasNewItem, newItem, addNewItem, projectList, updateNewItemSubtitle, updateNewItemEntireSubject, updateNewItemProject, updateSelectedItem] = useItemStore(state => [state.items, state.hasNewItem, state.newItem, state.addNewItem, state.projectList, state.updateNewItemSubtitle, state.updateNewItemEntireSubject, state.updateNewItemProject, state.updateSelectedItem]); const [subjects] = useSubjectStore(state => [state.subjects]); - const selectableHeaderStyle = { - width: 100 - }; const getItemsList = (item: StateItem, index: number) => { const { id, selected, subtitle, entireSubject, projectName } = item; @@ -106,10 +103,10 @@ function ItemList(): React.ReactElement { - Selected - Project - Full Subject? - Subtitle + SELECTED + PROJECT + FULL SUBJECT? + NAME/SUBTITLE diff --git a/client/src/store/metadata/metadata.defaults.ts b/client/src/store/metadata/metadata.defaults.ts index b085b210f..f39517af3 100644 --- a/client/src/store/metadata/metadata.defaults.ts +++ b/client/src/store/metadata/metadata.defaults.ts @@ -58,7 +58,7 @@ const selectedSubtitleValidation = { test: array => { const selectedSubtitle = array.find(subtitle => subtitle.selected); if (!selectedSubtitle) return false; - if (selectedSubtitle.subtitleOption !== eSubtitleOption.eNone) + if (selectedSubtitle.subtitleOption === eSubtitleOption.eInput) return !!selectedSubtitle.value; return true; }, diff --git a/client/src/store/utils.ts b/client/src/store/utils.ts index 177b672b6..06a1210b0 100644 --- a/client/src/store/utils.ts +++ b/client/src/store/utils.ts @@ -134,7 +134,7 @@ export function parseSubtitlesToState(titles: IngestTitle): SubtitleFields { result.push({ value: '', selected: true, subtitleOption: eSubtitleOption.eInput, id: key }); } // Inherited Value - if (subtitleVal && subtitleVal !== '') { + if (typeof subtitleVal === 'string' && subtitleVal !== '') { result.push({ value: subtitleVal, selected: false, subtitleOption: eSubtitleOption.eInherit, id: key }); } From 98b7078a384e06b75a71d02ed1775ff0db41fd44 Mon Sep 17 00:00:00 2001 From: Jon Tyson <6943745+jahjedtieson@users.noreply.github.com> Date: Mon, 28 Mar 2022 16:03:26 -0700 Subject: [PATCH 21/31] GraphQL: * Add name to IngestItemInput * Use supplied name in getIngestTitle when a user is creating a new item System: * When computing model title options, provide a special value of if the source item has a title (i.e. allow the user to explicitly remove the title) --- client/src/types/graphql.tsx | 1 + server/graphql/schema.graphql | 1 + server/graphql/schema/ingestion/mutations.graphql | 1 + .../schema/ingestion/resolvers/queries/getIngestTitle.ts | 4 ++-- server/types/graphql.ts | 1 + server/utils/nameHelpers.ts | 8 +++++--- 6 files changed, 11 insertions(+), 5 deletions(-) diff --git a/client/src/types/graphql.tsx b/client/src/types/graphql.tsx index 9660821d2..adabc2037 100644 --- a/client/src/types/graphql.tsx +++ b/client/src/types/graphql.tsx @@ -942,6 +942,7 @@ export type IngestProjectInput = { export type IngestItemInput = { id?: Maybe; + name?: Maybe; subtitle: Scalars['String']; entireSubject: Scalars['Boolean']; }; diff --git a/server/graphql/schema.graphql b/server/graphql/schema.graphql index f5f362819..262c8b282 100644 --- a/server/graphql/schema.graphql +++ b/server/graphql/schema.graphql @@ -535,6 +535,7 @@ input IngestProjectInput { input IngestItemInput { id: Int + name: String subtitle: String! entireSubject: Boolean! } diff --git a/server/graphql/schema/ingestion/mutations.graphql b/server/graphql/schema/ingestion/mutations.graphql index 508417cc9..94ae82ad3 100644 --- a/server/graphql/schema/ingestion/mutations.graphql +++ b/server/graphql/schema/ingestion/mutations.graphql @@ -17,6 +17,7 @@ input IngestProjectInput { input IngestItemInput { id: Int + name: String subtitle: String! entireSubject: Boolean! } diff --git a/server/graphql/schema/ingestion/resolvers/queries/getIngestTitle.ts b/server/graphql/schema/ingestion/resolvers/queries/getIngestTitle.ts index db6082455..09453b670 100644 --- a/server/graphql/schema/ingestion/resolvers/queries/getIngestTitle.ts +++ b/server/graphql/schema/ingestion/resolvers/queries/getIngestTitle.ts @@ -19,9 +19,9 @@ export default async function getIngestTitle(_: Parent, args: QueryGetIngestTitl idItem: 0, idAssetThumbnail: null, idGeoLocation: null, - Name: item.subtitle, + Name: item.name + ((item.subtitle) ? `: ${item.subtitle}` : ''), EntireSubject: item.entireSubject, - Title: null, + Title: item.subtitle, }); } const ingestTitle: IngestTitle = NameHelpers.modelTitleOptions(itemDB); diff --git a/server/types/graphql.ts b/server/types/graphql.ts index e3be5ca13..e5a342dd8 100644 --- a/server/types/graphql.ts +++ b/server/types/graphql.ts @@ -939,6 +939,7 @@ export type IngestProjectInput = { export type IngestItemInput = { id?: Maybe; + name?: Maybe; subtitle: Scalars['String']; entireSubject: Scalars['Boolean']; }; diff --git a/server/utils/nameHelpers.ts b/server/utils/nameHelpers.ts index d62f9cd4e..fc07f6a79 100644 --- a/server/utils/nameHelpers.ts +++ b/server/utils/nameHelpers.ts @@ -50,9 +50,11 @@ export class NameHelpers { static modelTitleOptions(item: DBAPI.Item): IngestTitle { const title: string = (item.Title) ? item.Name.replace(`: ${item.Title}`, '') : item.Name; // base title is the item's display name, with its subtitle removed, if any const subtitle: (string | null)[] = []; - subtitle.push(item.Title); // user can select the default item subtitle. - if (item.Title !== null) // if we record an entry with a real or empty subtitle, - subtitle.push(null); // provide an entry with null subtitle, indicating the user can enter one + subtitle.push(item.Title); // user can select the default item subtitle. + if (item.Title) // if we record an entry with a realsubtitle, + subtitle.push(''); // allow user to pick "None" + if (item.Title !== null) // if we record an entry with a real or empty subtitle, + subtitle.push(null); // provide an entry with null subtitle, indicating the user can enter one return { title, forced: false, subtitle }; } From 1a546a63e3186ad53d70cb6ab1f1d534f2c25d4a Mon Sep 17 00:00:00 2001 From: Hsin Tung Date: Mon, 28 Mar 2022 16:33:54 -0700 Subject: [PATCH 22/31] WIP addressing use cases for subtitle control --- .../Metadata/Control/SubtitleControl.tsx | 35 ++++++++++++++++++- .../Ingestion/components/Metadata/index.tsx | 8 ++++- client/src/store/metadata/index.ts | 5 +-- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/client/src/pages/Ingestion/components/Metadata/Control/SubtitleControl.tsx b/client/src/pages/Ingestion/components/Metadata/Control/SubtitleControl.tsx index 772642df8..732829977 100644 --- a/client/src/pages/Ingestion/components/Metadata/Control/SubtitleControl.tsx +++ b/client/src/pages/Ingestion/components/Metadata/Control/SubtitleControl.tsx @@ -61,6 +61,7 @@ function SubtitleControl(props: SubtitleControlProps): React.ReactElement { const renderSubtitleOptions = (subtitles: SubtitleFields): React.ReactElement => { + console.log('subtitles', subtitles,'objectName', objectName); // Case: forced if (subtitles.some(option => option.subtitleOption === eSubtitleOption.eForced)) return ( @@ -74,7 +75,39 @@ function SubtitleControl(props: SubtitleControlProps): React.ReactElement { ); - // Case: Name input only + // Case: optional subtitle + if (objectName && subtitles.length === 1 && subtitles.find(option => option.subtitleOption === eSubtitleOption.eInput)) { + const { id, value } = subtitles[0]; + return ( + <> + + + Name: + + + {`${objectName}${selectedSubtitlesName}`} + + + + + Subtitle: + + + onUpdateCustomSubtitle(e, id)} + element='input' + value={value} + className={classes.input} + debounceTimeout={400} + title={`subtitle-input-${value}`} + /> + + + + ); + } + + // Case: mandatory name input if (subtitles.length === 1 && subtitles.find(option => option.subtitleOption === eSubtitleOption.eInput)) { const { id, value } = subtitles[0]; return ( diff --git a/client/src/pages/Ingestion/components/Metadata/index.tsx b/client/src/pages/Ingestion/components/Metadata/index.tsx index 4db1e34fa..73084d575 100644 --- a/client/src/pages/Ingestion/components/Metadata/index.tsx +++ b/client/src/pages/Ingestion/components/Metadata/index.tsx @@ -257,7 +257,13 @@ function BreadcrumbsHeader(props: BreadcrumbsHeaderProps) { content = ( }> Specify metadata for: Project: {projectName} - Media Group: {subjects.length > 1 ? item?.subtitle : item?.id === 'default' ? `${subjects?.[0]?.name}${item?.subtitle ? `: ${item?.subtitle}` : ''}` : subjects?.[0]?.name} + {/* + Case 1: more than 1 subject - show entered name + Case 2: existing item is selected - show full name of that item + Case 3: new item w/o subtitle - [subject]: [subtitle] + Case 4: new item w/ subtitle - [subject] + */} + Media Group: {subjects.length > 1 ? item?.subtitle : item?.id === 'default' ? `${subjects?.[0]?.name}${item?.subtitle ? `: ${item?.subtitle}` : ''}` : item?.subtitle} {metadata?.file?.name} ); diff --git a/client/src/store/metadata/index.ts b/client/src/store/metadata/index.ts index 89d09be09..b307bb423 100644 --- a/client/src/store/metadata/index.ts +++ b/client/src/store/metadata/index.ts @@ -419,7 +419,7 @@ export const useMetadataStore = create((set: SetState((set: SetState Date: Mon, 28 Mar 2022 23:14:53 -0700 Subject: [PATCH 23/31] resolved how subtitle entries are rendered and made UI tweaks --- .../Metadata/Control/SubtitleControl.tsx | 22 +++++------ .../Metadata/Model/AssetFilesTable.tsx | 3 +- .../components/Metadata/Model/index.tsx | 1 - .../components/Metadata/Scene/index.tsx | 5 +++ .../components/SubjectItem/ItemList.tsx | 2 +- .../components/SubjectItem/index.tsx | 2 +- client/src/pages/Ingestion/hooks/useIngest.ts | 7 ++-- client/src/store/metadata/index.ts | 37 ++++++++++++++++++- client/src/store/repository.ts | 3 +- .../resolvers/mutations/ingestData.ts | 2 +- server/utils/nameHelpers.ts | 6 +-- 11 files changed, 65 insertions(+), 25 deletions(-) diff --git a/client/src/pages/Ingestion/components/Metadata/Control/SubtitleControl.tsx b/client/src/pages/Ingestion/components/Metadata/Control/SubtitleControl.tsx index 732829977..4ed524626 100644 --- a/client/src/pages/Ingestion/components/Metadata/Control/SubtitleControl.tsx +++ b/client/src/pages/Ingestion/components/Metadata/Control/SubtitleControl.tsx @@ -29,10 +29,10 @@ const useStyles = makeStyles(({ palette, typography }) => ({ cell: { border: 'none', padding: '1px 10px', - height: 30 + maxHeight: 22 }, labelCell: { - width: 50 + width: 30 }, optionContainer: { display: 'flex', @@ -40,7 +40,7 @@ const useStyles = makeStyles(({ palette, typography }) => ({ alignItems: 'center' }, input: { - height: 18, + height: 20, border: `1px solid ${fade(palette.primary.contrastText, 0.4)}`, fontFamily: typography.fontFamily, fontSize: '0.8rem', @@ -48,7 +48,7 @@ const useStyles = makeStyles(({ palette, typography }) => ({ borderRadius: 5, }, text: { - fontSize: '0.75rem' + fontSize: '0.8rem' } })); @@ -69,7 +69,7 @@ function SubtitleControl(props: SubtitleControlProps): React.ReactElement { Name: - + {`${objectName}${selectedSubtitlesName}`} @@ -84,7 +84,7 @@ function SubtitleControl(props: SubtitleControlProps): React.ReactElement { Name: - + {`${objectName}${selectedSubtitlesName}`} @@ -92,7 +92,7 @@ function SubtitleControl(props: SubtitleControlProps): React.ReactElement { Subtitle: - + onUpdateCustomSubtitle(e, id)} element='input' @@ -115,7 +115,7 @@ function SubtitleControl(props: SubtitleControlProps): React.ReactElement { Name: - + onUpdateCustomSubtitle(e, id)} element='input' @@ -136,7 +136,7 @@ function SubtitleControl(props: SubtitleControlProps): React.ReactElement { Name: - + {`${objectName}${selectedSubtitlesName}`} @@ -144,8 +144,8 @@ function SubtitleControl(props: SubtitleControlProps): React.ReactElement { Subtitle: - -
+ +
{ sortedSubtitles.map(({ selected, value, id, subtitleOption }, key) => (
diff --git a/client/src/pages/Ingestion/components/Metadata/Model/AssetFilesTable.tsx b/client/src/pages/Ingestion/components/Metadata/Model/AssetFilesTable.tsx index 00eebbd9c..0d8b69ec9 100644 --- a/client/src/pages/Ingestion/components/Metadata/Model/AssetFilesTable.tsx +++ b/client/src/pages/Ingestion/components/Metadata/Model/AssetFilesTable.tsx @@ -7,7 +7,8 @@ import { Table, TableBody, TableCell, TableHead, TableRow, Typography, Box } fro const useStyles = makeStyles(theme => ({ assetFilesTableContainer: { - width: 'calc(52vw + 20px)', + width: 'fit-content', + minWidth: 400, borderRadius: 5, padding: 1, backgroundColor: theme.palette.secondary.light diff --git a/client/src/pages/Ingestion/components/Metadata/Model/index.tsx b/client/src/pages/Ingestion/components/Metadata/Model/index.tsx index 56d8885af..cedc1b7f5 100644 --- a/client/src/pages/Ingestion/components/Metadata/Model/index.tsx +++ b/client/src/pages/Ingestion/components/Metadata/Model/index.tsx @@ -358,7 +358,6 @@ function Model(props: ModelProps): React.ReactElement {
- Date Created diff --git a/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx b/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx index 2452f3fd9..7c7570841 100644 --- a/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx +++ b/client/src/pages/Ingestion/components/Metadata/Scene/index.tsx @@ -195,6 +195,8 @@ function Scene(props: SceneProps): React.ReactElement { toast.error('Failed to fetch titles for ingestion items'); return; } + // console.log('ingestTitle', ingestTitle); + // console.log('sourceObjects', updatedSourceObjects); const subtitleState = parseSubtitlesToState(ingestTitle); updateMetadataField(metadataIndex, 'subtitles', subtitleState, MetadataType.scene); updateMetadataField(metadataIndex, 'name', ingestTitle.title, MetadataType.scene); @@ -231,6 +233,9 @@ function Scene(props: SceneProps): React.ReactElement { toast.error('Failed to fetch titles for ingestion items'); return; } + + // console.log('ingestTitle', ingestTitle); + // console.log('sourceObjects', newSourceObjects); const subtitleState = parseSubtitlesToState(ingestTitle); updateMetadataField(metadataIndex, 'subtitles', subtitleState, MetadataType.scene); updateMetadataField(metadataIndex, 'name', ingestTitle.title, MetadataType.scene); diff --git a/client/src/pages/Ingestion/components/SubjectItem/ItemList.tsx b/client/src/pages/Ingestion/components/SubjectItem/ItemList.tsx index e1e6161f9..c06f7c48a 100644 --- a/client/src/pages/Ingestion/components/SubjectItem/ItemList.tsx +++ b/client/src/pages/Ingestion/components/SubjectItem/ItemList.tsx @@ -234,7 +234,7 @@ function ItemListNewItem(props: ItemListNewItemProps) { {hasMultipleSubjects ? ( - No + No ) : (