Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ec90095f0 | |||
| 20fad26fef | |||
| 03215f79c4 | |||
| 6d17377ca2 | |||
| 256abaed0c | |||
| 26409b4ea6 | |||
| 865c4c1608 | |||
| 67836c2a7f | |||
| 95d819f7f3 | |||
| dee3ee6c4d | |||
| 7ed612ec14 | |||
| 4bd7af4c31 | |||
| 3ed2e977df | |||
| 788fa1c532 | |||
| bafb7df6ed | |||
| be4e4df983 | |||
| b4830c80ab | |||
| a2d97facc5 | |||
| aea344a46a | |||
| 13aa65bc2e | |||
| 0a307c28e0 | |||
| e0f4a4c02c | |||
| 29773f9492 | |||
| 8f67ef4c84 | |||
| 2c5f192403 | |||
| 0c58789dd6 | |||
| 82acce2abb | |||
| d83472c30b | |||
| 85f8f72335 |
@@ -150,7 +150,7 @@ shared Speckle.GetWorkspace = Value.ReplaceType(
|
||||
|
||||
shared Speckle.Objects.Properties = Value.ReplaceType(
|
||||
Speckle.LoadFunction("Objects.Properties.pqm"),
|
||||
type function (inputRecord as record, optional filterKeys as list) as record
|
||||
type function (inputRecord as any, optional filterKeys as list, optional parentPath as text, optional existingFields as list) as record
|
||||
);
|
||||
|
||||
|
||||
@@ -184,6 +184,11 @@ shared Speckle.Models.Federate = Value.ReplaceType(
|
||||
type function (tables as list, optional excludeData as logical) as table
|
||||
);
|
||||
|
||||
shared Speckle.Models.MaterialQuantities = Value.ReplaceType(
|
||||
Speckle.LoadFunction("Models.MaterialQuantities.pqm"),
|
||||
type function (inputTable as table, optional addPrefix as logical) as table
|
||||
);
|
||||
|
||||
[DataSource.Kind = "Speckle", Publish="GetByUrl.Publish"]
|
||||
shared Speckle.GetByUrl = Value.ReplaceType(
|
||||
Speckle.LoadFunction("GetByUrl.pqm"),
|
||||
@@ -242,7 +247,7 @@ GetByUrl.Icons = [
|
||||
Speckle = [
|
||||
// This is used when running the connector on an on-premises data gateway
|
||||
TestConnection = (path) => {"Speckle.GetUser", path},
|
||||
// Authentication strategy
|
||||
// Authentication strategy - OAuth only
|
||||
Authentication = [
|
||||
OAuth = [
|
||||
Label = "Speckle Account",
|
||||
@@ -352,13 +357,6 @@ Speckle = [
|
||||
]
|
||||
in
|
||||
result
|
||||
],
|
||||
Key = [
|
||||
KeyLabel = "Personal Access Token",
|
||||
Label = "Private Project"
|
||||
],
|
||||
Implicit = [
|
||||
Label = "Public Project"
|
||||
]
|
||||
],
|
||||
Label = "Speckle"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
(server as text, optional query as text, optional variables as record) as record =>
|
||||
let
|
||||
// Enhanced credential retrieval with OAuth2 support
|
||||
apiKey = try Extension.CurrentCredential()[Key] otherwise try Extension.CurrentCredential()[access_token] otherwise null,
|
||||
apiKey = try Extension.CurrentCredential()[access_token] otherwise null,
|
||||
|
||||
defaultQuery = "query {
|
||||
activeUser {
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
versionId = parsedUrl[versionId],
|
||||
|
||||
// get API key if available
|
||||
apiKey = try Extension.CurrentCredential()[Key] otherwise null,
|
||||
apiKey = try Extension.CurrentCredential()[access_token] otherwise null,
|
||||
|
||||
// graphql query to get model info including root object id
|
||||
// includes specific version if provided
|
||||
|
||||
@@ -26,7 +26,7 @@ in
|
||||
parsedUrl = Parser(url),
|
||||
server = parsedUrl[baseUrl],
|
||||
|
||||
apiKey = try Extension.CurrentCredential()[Key] otherwise try Extension.CurrentCredential()[access_token] otherwise "",
|
||||
apiKey = try Extension.CurrentCredential()[access_token] otherwise "",
|
||||
|
||||
query = "query {
|
||||
activeUser {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// function for transforming a table to extract and expand Material Quantities data
|
||||
(inputTable as table, optional addPrefix as logical) as table =>
|
||||
let
|
||||
// Default addPrefix to false if not provided
|
||||
UsePrefix = if addPrefix = null then false else addPrefix,
|
||||
|
||||
// Add mq column using existing MaterialQuantities function with list output
|
||||
AddedMQ = Table.AddColumn(inputTable, "mq", each Speckle.Objects.MaterialQuantities([data], true)),
|
||||
|
||||
// Expand the mq list column
|
||||
ExpandMQ = Table.ExpandListColumn(AddedMQ, "mq"),
|
||||
|
||||
// Add MQProperties column using Properties function with error handling
|
||||
AddedMQProperties = Table.AddColumn(ExpandMQ, "MQ", each try Speckle.Objects.Properties([mq]) otherwise null),
|
||||
|
||||
// Expand the MQProperties record using Utils.ExpandRecord
|
||||
ExpandMQProperties = Speckle.Utils.ExpandRecord(AddedMQProperties, "MQ", null, UsePrefix),
|
||||
|
||||
// Remove the temporary mq and MQProperties columns
|
||||
FinalTable = Table.RemoveColumns(ExpandMQProperties, {"mq", "MQ"}, MissingField.Ignore)
|
||||
in
|
||||
FinalTable
|
||||
@@ -1,196 +1,257 @@
|
||||
// function for extracting and flattening properties from Speckle objects
|
||||
(inputRecord as record, optional filterKeys as list) as record =>
|
||||
(inputRecord as any, optional filterKeys as list, optional parentPath as text, optional existingFields as list) as record =>
|
||||
let
|
||||
// auto-extract properties if the input has a "properties" field
|
||||
ActualInput = if Record.HasFields(inputRecord, {"properties"}) then
|
||||
inputRecord[properties]
|
||||
else
|
||||
inputRecord,
|
||||
|
||||
// helper function to check if a key should be included
|
||||
ShouldIncludeKey = (keyName as text) as logical =>
|
||||
if filterKeys = null then
|
||||
true
|
||||
else
|
||||
List.Contains(filterKeys, keyName),
|
||||
|
||||
// define excluded paths
|
||||
// Define excluded paths
|
||||
ExcludedPaths = {
|
||||
"Composite Structure",
|
||||
"Material Quantities",
|
||||
"Parameters.Type Parameters.Structure"
|
||||
},
|
||||
|
||||
IsExcludedPath = (path as text) as logical =>
|
||||
List.AnyTrue(
|
||||
List.Transform(
|
||||
ExcludedPaths,
|
||||
(excludedPath) => Text.StartsWith(path, excludedPath)
|
||||
)
|
||||
),
|
||||
// Helper function to check if a path should be excluded
|
||||
IsPathExcluded = (currentPath as text) as logical =>
|
||||
List.AnyTrue(List.Transform(ExcludedPaths, each Text.Contains(currentPath, _))),
|
||||
|
||||
// helper function to handle duplicate keys by adding suffixes
|
||||
AddUniqueKey = (existingRecord as record, newKey as text, newValue as any) as record =>
|
||||
// Helper function to resolve naming conflicts
|
||||
ResolveFieldName = (fieldName as text, parentPathParam as nullable text, existingFieldsParam as nullable list) as text =>
|
||||
let
|
||||
originalKey = newKey,
|
||||
counter = 1,
|
||||
// Ensure we have valid inputs
|
||||
parentPath = if parentPathParam = null then "" else parentPathParam,
|
||||
existingFields = if existingFieldsParam = null then {} else existingFieldsParam,
|
||||
|
||||
// find a unique key by adding suffix if needed
|
||||
FindUniqueKey = (testKey as text, testCounter as number) as text =>
|
||||
if Record.HasFields(existingRecord, {testKey}) then
|
||||
@FindUniqueKey(originalKey & "_" & Text.From(testCounter), testCounter + 1)
|
||||
else
|
||||
testKey,
|
||||
// Try original field name first
|
||||
candidateName = fieldName,
|
||||
|
||||
uniqueKey = FindUniqueKey(newKey, counter),
|
||||
result = Record.AddField(existingRecord, uniqueKey, newValue)
|
||||
// If no conflict, return original name
|
||||
finalName = if not List.Contains(existingFields, candidateName) then
|
||||
candidateName
|
||||
else if parentPath = "" then
|
||||
fieldName // No parent path available, keep original
|
||||
else
|
||||
let
|
||||
// Split parent path and try adding parents one by one
|
||||
pathParts = Text.Split(parentPath, "."),
|
||||
reversedParts = List.Reverse(pathParts), // Start with immediate parent
|
||||
|
||||
// Use iteration instead of recursion
|
||||
ResolveWithIteration = () =>
|
||||
let
|
||||
// Generate all possible candidates
|
||||
candidates = List.Generate(
|
||||
() => [depth = 1, candidate = fieldName & "." & List.First(reversedParts)],
|
||||
each [depth] <= List.Count(reversedParts),
|
||||
each [
|
||||
depth = [depth] + 1,
|
||||
candidate = fieldName & "." & Text.Combine(List.FirstN(reversedParts, [depth]), ".")
|
||||
],
|
||||
each [candidate]
|
||||
),
|
||||
|
||||
// Find first non-conflicting candidate
|
||||
firstNonConflicting = List.First(
|
||||
List.Select(candidates, each not List.Contains(existingFields, _)),
|
||||
// If all conflict, use full path
|
||||
fieldName & "." & Text.Combine(reversedParts, ".")
|
||||
)
|
||||
in
|
||||
firstNonConflicting,
|
||||
|
||||
resolvedName = ResolveWithIteration()
|
||||
in
|
||||
resolvedName
|
||||
in
|
||||
finalName,
|
||||
|
||||
// Create the main flattening function with self-reference capability
|
||||
FlattenRecordImpl = (
|
||||
flattenFn as function,
|
||||
inputRecord as any,
|
||||
filterKeys as nullable list,
|
||||
parentPathParam as nullable text,
|
||||
existingFieldsParam as nullable list
|
||||
) as record =>
|
||||
let
|
||||
// Ensure non-null values for internal use
|
||||
currentParentPath = if parentPathParam = null then "" else parentPathParam,
|
||||
currentExistingFields = if existingFieldsParam = null then {} else existingFieldsParam,
|
||||
currentfilterKeys = filterKeys,
|
||||
|
||||
// Check if record has "properties" field and use it instead of the root record
|
||||
recordToProcess = if inputRecord = null then
|
||||
null
|
||||
else if Value.Is(inputRecord, type record) and Record.HasFields(inputRecord, {"properties"}) then
|
||||
Record.Field(inputRecord, "properties")
|
||||
else
|
||||
inputRecord,
|
||||
|
||||
// Helper function to check if a field should be included
|
||||
ShouldIncludeField = (fieldName as text) as logical =>
|
||||
if currentfilterKeys = null then true
|
||||
else List.Contains(currentfilterKeys, fieldName),
|
||||
|
||||
// Handle different input types
|
||||
result = if recordToProcess = null then
|
||||
[]
|
||||
else if Value.Is(recordToProcess, type record) then
|
||||
let
|
||||
fieldNames = Record.FieldNames(recordToProcess),
|
||||
|
||||
// Process each field
|
||||
processedFields = List.Accumulate(
|
||||
fieldNames,
|
||||
[FlattenedRecord = [], ExistingFieldsList = currentExistingFields],
|
||||
(state, fieldName) =>
|
||||
let
|
||||
fieldValue = Record.Field(recordToProcess, fieldName),
|
||||
newPath = if currentParentPath = "" then fieldName else currentParentPath & "." & fieldName,
|
||||
|
||||
// Skip if path is excluded
|
||||
shouldProcess = not IsPathExcluded(newPath),
|
||||
|
||||
processResult = if not shouldProcess then
|
||||
state
|
||||
else
|
||||
let
|
||||
// Check if this is a name/value record
|
||||
hasNameValue = Value.Is(fieldValue, type record) and
|
||||
Record.HasFields(fieldValue, {"name", "value"}),
|
||||
|
||||
finalResult = if hasNameValue then
|
||||
let
|
||||
nameField = Record.Field(fieldValue, "name"),
|
||||
valueField = Record.Field(fieldValue, "value"),
|
||||
// Check if this name field should be included
|
||||
shouldInclude = if nameField = null then false else ShouldIncludeField(nameField),
|
||||
result = if shouldInclude and nameField <> null then
|
||||
let
|
||||
resolvedName = ResolveFieldName(nameField, currentParentPath, state[ExistingFieldsList]),
|
||||
newRecord = Record.AddField(state[FlattenedRecord], resolvedName, valueField),
|
||||
newFieldsList = state[ExistingFieldsList] & {resolvedName}
|
||||
in
|
||||
[FlattenedRecord = newRecord, ExistingFieldsList = newFieldsList]
|
||||
else
|
||||
state
|
||||
in
|
||||
result
|
||||
else if fieldValue = null then
|
||||
let
|
||||
shouldInclude = ShouldIncludeField(fieldName),
|
||||
result = if shouldInclude then
|
||||
let
|
||||
resolvedName = ResolveFieldName(fieldName, currentParentPath, state[ExistingFieldsList]),
|
||||
newRecord = Record.AddField(state[FlattenedRecord], resolvedName, null),
|
||||
newFieldsList = state[ExistingFieldsList] & {resolvedName}
|
||||
in
|
||||
[FlattenedRecord = newRecord, ExistingFieldsList = newFieldsList]
|
||||
else
|
||||
state
|
||||
in
|
||||
result
|
||||
else if Value.Is(fieldValue, type record) then
|
||||
let
|
||||
// Skip empty records
|
||||
fieldCount = Record.FieldCount(fieldValue),
|
||||
recursiveResult = if fieldCount = 0 then
|
||||
state
|
||||
else
|
||||
let
|
||||
// Call the function through the passed reference
|
||||
// IMPORTANT: Pass the current state's existing fields list
|
||||
flattened = flattenFn(flattenFn, fieldValue, currentfilterKeys, newPath, state[ExistingFieldsList]),
|
||||
// Get all field names from the flattened result
|
||||
flattenedFieldNames = Record.FieldNames(flattened),
|
||||
// Merge the flattened record with the current state
|
||||
combinedRecord = flattened & state[FlattenedRecord],
|
||||
// Update the existing fields list with ALL fields from both records
|
||||
allFieldNames = List.Distinct(state[ExistingFieldsList] & flattenedFieldNames)
|
||||
in
|
||||
[FlattenedRecord = combinedRecord, ExistingFieldsList = allFieldNames]
|
||||
in
|
||||
recursiveResult
|
||||
else if Value.Is(fieldValue, type list) then
|
||||
let
|
||||
listLength = List.Count(fieldValue),
|
||||
// Skip empty lists
|
||||
listResult = if listLength = 0 then
|
||||
state
|
||||
else
|
||||
List.Accumulate(
|
||||
List.Positions(fieldValue),
|
||||
state,
|
||||
(listState, index) =>
|
||||
let
|
||||
listItem = fieldValue{index},
|
||||
indexSuffix = Text.From(index + 1), // 1-based indexing
|
||||
listFieldName = fieldName & "." & indexSuffix,
|
||||
listPath = if currentParentPath = "" then listFieldName else currentParentPath & "." & listFieldName,
|
||||
|
||||
itemResult = if Value.Is(listItem, type record) then
|
||||
let
|
||||
itemFieldCount = Record.FieldCount(listItem),
|
||||
itemFlattened = if itemFieldCount = 0 then
|
||||
listState
|
||||
else
|
||||
let
|
||||
// Call the function through the passed reference
|
||||
flattened = flattenFn(flattenFn, listItem, currentfilterKeys, listPath, listState[ExistingFieldsList]),
|
||||
// Get all field names from the flattened result
|
||||
flattenedFieldNames = Record.FieldNames(flattened),
|
||||
// Merge the flattened record with the current state
|
||||
combinedRecord = flattened & listState[FlattenedRecord],
|
||||
// Update the existing fields list with ALL fields
|
||||
allFieldNames = List.Distinct(listState[ExistingFieldsList] & flattenedFieldNames)
|
||||
in
|
||||
[FlattenedRecord = combinedRecord, ExistingFieldsList = allFieldNames]
|
||||
in
|
||||
itemFlattened
|
||||
else
|
||||
let
|
||||
shouldInclude = ShouldIncludeField(listFieldName),
|
||||
result = if shouldInclude then
|
||||
let
|
||||
resolvedName = ResolveFieldName(listFieldName, currentParentPath, listState[ExistingFieldsList]),
|
||||
newRecord = Record.AddField(listState[FlattenedRecord], resolvedName, listItem),
|
||||
newFieldsList = listState[ExistingFieldsList] & {resolvedName}
|
||||
in
|
||||
[FlattenedRecord = newRecord, ExistingFieldsList = newFieldsList]
|
||||
else
|
||||
listState
|
||||
in
|
||||
result
|
||||
in
|
||||
itemResult
|
||||
)
|
||||
in
|
||||
listResult
|
||||
else
|
||||
// Handle primitive values
|
||||
let
|
||||
shouldInclude = ShouldIncludeField(fieldName),
|
||||
result = if shouldInclude then
|
||||
let
|
||||
resolvedName = ResolveFieldName(fieldName, currentParentPath, state[ExistingFieldsList]),
|
||||
newRecord = Record.AddField(state[FlattenedRecord], resolvedName, fieldValue),
|
||||
newFieldsList = state[ExistingFieldsList] & {resolvedName}
|
||||
in
|
||||
[FlattenedRecord = newRecord, ExistingFieldsList = newFieldsList]
|
||||
else
|
||||
state
|
||||
in
|
||||
result
|
||||
in
|
||||
finalResult
|
||||
in
|
||||
processResult
|
||||
)
|
||||
in
|
||||
processedFields[FlattenedRecord]
|
||||
else
|
||||
// If input is not a record, return it as is in a record wrapper
|
||||
[Value = recordToProcess]
|
||||
in
|
||||
result,
|
||||
|
||||
// enhanced combine function that handles duplicates and filtering
|
||||
SafeRecordCombine = (records as list) as record =>
|
||||
List.Accumulate(
|
||||
records,
|
||||
[],
|
||||
(state, current) =>
|
||||
List.Accumulate(
|
||||
Record.FieldNames(current),
|
||||
state,
|
||||
(innerState, fieldName) =>
|
||||
if ShouldIncludeKey(fieldName) then
|
||||
AddUniqueKey(innerState, fieldName, Record.Field(current, fieldName))
|
||||
else
|
||||
innerState
|
||||
)
|
||||
),
|
||||
|
||||
// helper function to process name-value objects
|
||||
ProcessNameValueObject = (obj as record) as record =>
|
||||
let
|
||||
// Assert that the object has required fields
|
||||
_ = if not (Record.HasFields(obj, {"name"}) and Record.HasFields(obj, {"value"})) then
|
||||
error [
|
||||
Reason = "Invalid Name-Value Object",
|
||||
Message = "Object must have both 'name' and 'value' fields"
|
||||
]
|
||||
else
|
||||
null,
|
||||
|
||||
BaseName = obj[name],
|
||||
BaseValue = obj[value],
|
||||
|
||||
// only extract name and value if it should be included
|
||||
Result = if ShouldIncludeKey(BaseName) then
|
||||
Record.FromList({BaseValue}, {BaseName})
|
||||
else
|
||||
[]
|
||||
in
|
||||
Result,
|
||||
|
||||
// helper function to process direct key-value objects
|
||||
ProcessDirectKeyValueObject = (obj as record) as record =>
|
||||
let
|
||||
// assert that input is a record
|
||||
_ = if not Value.Is(obj, type record) then
|
||||
error [
|
||||
Reason = "Invalid Input Type",
|
||||
Message = "Expected record for direct key-value processing"
|
||||
]
|
||||
else
|
||||
null,
|
||||
|
||||
// extract all primitive key-value pairs
|
||||
PrimitiveFields = List.Transform(
|
||||
Record.FieldNames(obj),
|
||||
(fieldName) =>
|
||||
let
|
||||
fieldValue = Record.Field(obj, fieldName)
|
||||
in
|
||||
if not Type.Is(Value.Type(fieldValue), type record) and ShouldIncludeKey(fieldName) then
|
||||
Record.FromList({fieldValue}, {fieldName})
|
||||
else
|
||||
[]
|
||||
),
|
||||
|
||||
// filter out empty records and combine using safe combine
|
||||
FilteredFields = List.Select(PrimitiveFields, (rec) => Record.FieldCount(rec) > 0),
|
||||
Result = SafeRecordCombine(FilteredFields)
|
||||
in
|
||||
Result,
|
||||
|
||||
// helper functions for type checking
|
||||
HasDirectKeyValuePattern = (obj as record) as logical =>
|
||||
List.AllTrue(
|
||||
List.Transform(
|
||||
Record.FieldValues(obj),
|
||||
(value) => not Type.Is(Value.Type(value), type record)
|
||||
)
|
||||
),
|
||||
|
||||
IsNestedContainer = (obj as record) as logical =>
|
||||
List.AnyTrue(
|
||||
List.Transform(
|
||||
Record.FieldValues(obj),
|
||||
(value) => Type.Is(Value.Type(value), type record)
|
||||
)
|
||||
),
|
||||
|
||||
// main processing function with path tracking
|
||||
ProcessField = (fieldName as text, fieldValue as any, currentPath as text) as record =>
|
||||
let
|
||||
fieldPath = if currentPath = "" then fieldName else currentPath & "." & fieldName
|
||||
in
|
||||
if IsExcludedPath(fieldPath) then
|
||||
[]
|
||||
else if Type.Is(Value.Type(fieldValue), type record) then
|
||||
if Record.HasFields(fieldValue, {"name", "value"}) then
|
||||
ProcessNameValueObject(fieldValue)
|
||||
else if HasDirectKeyValuePattern(fieldValue) then
|
||||
ProcessDirectKeyValueObject(fieldValue)
|
||||
else if IsNestedContainer(fieldValue) then
|
||||
// recursive call for nested containers
|
||||
@ProcessRecord(fieldValue, fieldPath)
|
||||
else
|
||||
// unknown record type, skip
|
||||
[]
|
||||
else
|
||||
// primitive value - check if should be included
|
||||
if ShouldIncludeKey(fieldName) then
|
||||
Record.FromList({fieldValue}, {fieldName})
|
||||
else
|
||||
[],
|
||||
|
||||
// process entire record recursively
|
||||
ProcessRecord = (record as record, currentPath as text) as record =>
|
||||
let
|
||||
// assert that input is a record
|
||||
_ = if not Value.Is(record, type record) then
|
||||
error [
|
||||
Reason = "Invalid Record Type",
|
||||
Message = "Expected record for processing, but received: " & Text.From(Value.Type(record))
|
||||
]
|
||||
else
|
||||
null,
|
||||
|
||||
ProcessedFields = List.Transform(
|
||||
Record.FieldNames(record),
|
||||
(fieldName) => ProcessField(fieldName, Record.Field(record, fieldName), currentPath)
|
||||
),
|
||||
FilteredFields = List.Select(ProcessedFields, (rec) => Record.FieldCount(rec) > 0),
|
||||
CombinedRecord = SafeRecordCombine(FilteredFields)
|
||||
in
|
||||
CombinedRecord,
|
||||
|
||||
// assert that input is a record before processing
|
||||
_ = if not Value.Is(ActualInput, type record) then
|
||||
error [
|
||||
Reason = "Invalid Input Type",
|
||||
Message = "Input must be a record, but received: " & Text.From(Value.Type(ActualInput))
|
||||
]
|
||||
else
|
||||
null,
|
||||
|
||||
// start processing from root
|
||||
Result = ProcessRecord(ActualInput, "")
|
||||
// Call the implementation with self-reference
|
||||
result = FlattenRecordImpl(FlattenRecordImpl, inputRecord, filterKeys, parentPath, existingFields)
|
||||
in
|
||||
Result
|
||||
result
|
||||
@@ -37,16 +37,40 @@
|
||||
|
||||
workspaceInfo = GetWorkspace(url),
|
||||
|
||||
// Prepare request data
|
||||
// exchange powerful token for weak token via ds
|
||||
tokenExchangeData = Json.FromValue([
|
||||
PowerfulToken = apiKey,
|
||||
Scopes = {"profile:read", "streams:read", "users:read"},
|
||||
ProjectId = parsedUrl[projectId],
|
||||
ServerUrl = parsedUrl[baseUrl]
|
||||
]),
|
||||
|
||||
tokenExchangeResponse = Web.Contents(
|
||||
"http://127.0.0.1:29364/auth/exchange-token",
|
||||
[
|
||||
Headers = [
|
||||
#"Content-Type" = "application/json",
|
||||
#"Method" = "POST"
|
||||
],
|
||||
Content = tokenExchangeData,
|
||||
ManualStatusHandling = {400, 401, 403, 404, 500}
|
||||
]
|
||||
),
|
||||
|
||||
tokenExchangeJson = Json.Document(tokenExchangeResponse),
|
||||
weakToken = tokenExchangeJson[token],
|
||||
|
||||
// prepare request data with weak token
|
||||
requestData = Json.FromValue([
|
||||
Url = url,
|
||||
Server = parsedUrl[baseUrl],
|
||||
Email = userEmail,
|
||||
ProjectId = parsedUrl[projectId],
|
||||
ObjectId = modelInfo[rootObjectId],
|
||||
RootObjectId = modelInfo[rootObjectId],
|
||||
SourceApplication = modelInfo[sourceApplication],
|
||||
Token = apiKey,
|
||||
Token = weakToken,
|
||||
Version = connectorVersion,
|
||||
VersionId = parsedUrl[versionId],
|
||||
WorkspaceId = workspaceInfo[workspaceId],
|
||||
WorkspaceName = workspaceInfo[workspaceName],
|
||||
WorkspaceLogo = workspaceInfo[workspaceLogo],
|
||||
|
||||
@@ -133,6 +133,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dataLoading": {
|
||||
"properties": {
|
||||
"internalizeData": {
|
||||
"type": {
|
||||
"bool": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"color": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
|
||||
Generated
+334
-12
@@ -13,7 +13,8 @@
|
||||
"@babel/runtime-corejs3": "^7.21.5",
|
||||
"@headlessui/vue": "^1.7.13",
|
||||
"@heroicons/vue": "^2.0.12",
|
||||
"@speckle/objectloader": "^2.23.8",
|
||||
"@speckle/objectloader": "^2.25.9",
|
||||
"@speckle/objectloader2": "^2.25.9",
|
||||
"@speckle/tailwind-theme": "2.23.2",
|
||||
"@speckle/ui-components": "2.23.2",
|
||||
"@speckle/viewer": "2.23.23",
|
||||
@@ -2826,6 +2827,13 @@
|
||||
"deprecated": "Use @eslint/object-schema instead",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@ioredis/commands": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.3.0.tgz",
|
||||
"integrity": "sha512-M/T6Zewn7sDaBQEqIZ8Rb+i9y8qfGmq+5SDFSf9sA2lUZTmdDLVdOiQaeDp+Q4wElZ9HG1GAX5KhDaidp6LQsQ==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
@@ -2979,6 +2987,90 @@
|
||||
"integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
|
||||
"integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz",
|
||||
"integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz",
|
||||
"integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz",
|
||||
"integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz",
|
||||
"integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz",
|
||||
"integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
|
||||
"version": "5.1.1-v1",
|
||||
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
|
||||
@@ -3052,12 +3144,13 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@speckle/objectloader": {
|
||||
"version": "2.23.23",
|
||||
"resolved": "https://registry.npmjs.org/@speckle/objectloader/-/objectloader-2.23.23.tgz",
|
||||
"integrity": "sha512-k0qxk5M0Q57h+fth6GQq8N7SjeJnWHxjDlMDYC56lOZkH8vI0Y2RHG33+DiBvC7iHnTIRuZc0SyTRrJ64Cuhrg==",
|
||||
"version": "2.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@speckle/objectloader/-/objectloader-2.25.9.tgz",
|
||||
"integrity": "sha512-ZSMinqrHm4Hx3x6dth2kfnJO2O1zI0i4E0eE3PS9hg1HAtCx9opIT/eKIzj5fYY/cUyfXxP00k7qXmZ3KAUl7w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.17.9",
|
||||
"@speckle/shared": "^2.23.23",
|
||||
"@speckle/shared": "^2.25.9",
|
||||
"core-js": "^3.21.1",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
@@ -3067,11 +3160,25 @@
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@speckle/shared": {
|
||||
"version": "2.23.23",
|
||||
"resolved": "https://registry.npmjs.org/@speckle/shared/-/shared-2.23.23.tgz",
|
||||
"integrity": "sha512-5laonEcP7FsG5CPn/EXq0tpkA135Bxq3TndZ+OJGzuaY9L6ZHLtSURltZ6YpjMo6CHmAiFEVG62laP6UHwIL4w==",
|
||||
"node_modules/@speckle/objectloader2": {
|
||||
"version": "2.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@speckle/objectloader2/-/objectloader2-2.25.9.tgz",
|
||||
"integrity": "sha512-fZYkVGBCNUcVMMZnIljmtqiyXpSlyWiuW+qbpfls5lX6P9ZLP6DC88AUjyJQ6cM9jZ6RGNk3/Sa+MeReTeZIvg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@speckle/shared": "^2.25.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@speckle/shared": {
|
||||
"version": "2.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@speckle/shared/-/shared-2.25.9.tgz",
|
||||
"integrity": "sha512-7hK6v55tSu8OTxza9UakettBpzlbyIWq17jtzjf3ZSnqqjiasayhG3O0/uuRI2GoeingoOqUMLvybatjDu4ang==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"dayjs": "^1.11.13",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nanoid": "^5.1.5",
|
||||
@@ -3083,6 +3190,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.0.0-beta.176",
|
||||
"bull": "*",
|
||||
"knex": "*",
|
||||
"mixpanel": "^0.17.0",
|
||||
"pino": "^8.7.0",
|
||||
@@ -5271,6 +5379,38 @@
|
||||
"integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/bull": {
|
||||
"version": "4.16.5",
|
||||
"resolved": "https://registry.npmjs.org/bull/-/bull-4.16.5.tgz",
|
||||
"integrity": "sha512-lDsx2BzkKe7gkCYiT5Acj02DpTwDznl/VNN7Psn7M3USPG7Vs/BaClZJJTAG+ufAR9++N1/NiUTdaFBWDIl5TQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cron-parser": "^4.9.0",
|
||||
"get-port": "^5.1.1",
|
||||
"ioredis": "^5.3.2",
|
||||
"lodash": "^4.17.21",
|
||||
"msgpackr": "^1.11.2",
|
||||
"semver": "^7.5.2",
|
||||
"uuid": "^8.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/bull/node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/bundle-name": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
|
||||
@@ -5526,6 +5666,16 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/cluster-key-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -5845,6 +5995,19 @@
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/cron-parser": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz",
|
||||
"integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"luxon": "^3.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -6076,6 +6239,12 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.13",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/de-indent": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||
@@ -6198,6 +6367,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
@@ -6235,6 +6414,17 @@
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-node": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
|
||||
@@ -7741,6 +7931,19 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-port": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz",
|
||||
"integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
||||
@@ -8485,6 +8688,31 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/ioredis": {
|
||||
"version": "5.7.0",
|
||||
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.7.0.tgz",
|
||||
"integrity": "sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ioredis/commands": "^1.3.0",
|
||||
"cluster-key-slot": "^1.1.0",
|
||||
"debug": "^4.3.4",
|
||||
"denque": "^2.1.0",
|
||||
"lodash.defaults": "^4.2.0",
|
||||
"lodash.isarguments": "^3.1.0",
|
||||
"redis-errors": "^1.2.0",
|
||||
"redis-parser": "^3.0.0",
|
||||
"standard-as-callback": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.22.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/ioredis"
|
||||
}
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
|
||||
@@ -9262,8 +9490,14 @@
|
||||
"node_modules/lodash.defaults": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="
|
||||
},
|
||||
"node_modules/lodash.isarguments": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
|
||||
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
@@ -9327,6 +9561,16 @@
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/luxon": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.1.tgz",
|
||||
"integrity": "sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/lz-string": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||
@@ -9653,6 +9897,39 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/msgpackr": {
|
||||
"version": "1.11.5",
|
||||
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz",
|
||||
"integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"optionalDependencies": {
|
||||
"msgpackr-extract": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/msgpackr-extract": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz",
|
||||
"integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"node-gyp-build-optional-packages": "5.2.2"
|
||||
},
|
||||
"bin": {
|
||||
"download-msgpackr-prebuilds": "bin/download-prebuilds.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/multicast-dns": {
|
||||
"version": "7.2.5",
|
||||
"resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz",
|
||||
@@ -9773,6 +10050,22 @@
|
||||
"node": ">= 6.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build-optional-packages": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz",
|
||||
"integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"node-gyp-build-optional-packages": "bin.js",
|
||||
"node-gyp-build-optional-packages-optional": "optional.js",
|
||||
"node-gyp-build-optional-packages-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.19",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
||||
@@ -12547,6 +12840,29 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/redis-errors": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
|
||||
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/redis-parser": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
|
||||
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"redis-errors": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/regenerate": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
|
||||
@@ -13383,6 +13699,13 @@
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/standard-as-callback": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
|
||||
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
@@ -14486,7 +14809,6 @@
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
"@babel/runtime-corejs3": "^7.21.5",
|
||||
"@headlessui/vue": "^1.7.13",
|
||||
"@heroicons/vue": "^2.0.12",
|
||||
"@speckle/objectloader": "^2.23.8",
|
||||
"@speckle/objectloader": "^2.25.9",
|
||||
"@speckle/objectloader2": "^2.25.9",
|
||||
"@speckle/tailwind-theme": "2.23.2",
|
||||
"@speckle/ui-components": "2.23.2",
|
||||
"@speckle/viewer": "2.23.23",
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { useVisualStore } from '@src/store/visualStore'
|
||||
import ObjectLoader from '@speckle/objectloader' // Default import for v1
|
||||
|
||||
interface SpeckleObject {
|
||||
id: string
|
||||
speckle_type?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export class SpeckleApiLoader {
|
||||
private serverUrl: string
|
||||
private token: string
|
||||
private projectId: string
|
||||
private headers: Record<string, string>
|
||||
|
||||
constructor(serverUrl: string, projectId: string, token: string) {
|
||||
this.serverUrl = serverUrl.replace(/\/$/, '')
|
||||
this.projectId = projectId
|
||||
this.token = token
|
||||
this.headers = {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
|
||||
async downloadObjectsWithChildren(objectId: string, onProgress?: (loaded: number, total: number) => void): Promise<SpeckleObject[]> {
|
||||
const visualStore = useVisualStore()
|
||||
|
||||
visualStore.setLoadingProgress('Initializing object loader', 0)
|
||||
console.log('Creating ObjectLoader v1 for Power BI environment')
|
||||
|
||||
// Create ObjectLoader v1 instance - use 'token' not 'authToken'
|
||||
const loader = new ObjectLoader({
|
||||
serverUrl: this.serverUrl,
|
||||
streamId: this.projectId,
|
||||
objectId: objectId,
|
||||
token: this.token,
|
||||
options: {
|
||||
enableCaching: false, // Disable caching for Power BI environment
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
// Get total count for progress tracking
|
||||
const totalCount = await loader.getTotalObjectCount()
|
||||
console.log(`Loading ${totalCount} objects using ObjectLoader v1`)
|
||||
|
||||
const objects: SpeckleObject[] = []
|
||||
let loadedCount = 0
|
||||
|
||||
// Stream all objects using the async iterator
|
||||
for await (const obj of loader.getObjectIterator()) {
|
||||
objects.push(obj as SpeckleObject) // Type assertion since ObjectLoader v1 has different type
|
||||
loadedCount++
|
||||
|
||||
// Update progress
|
||||
if (onProgress) {
|
||||
onProgress(loadedCount, totalCount)
|
||||
}
|
||||
|
||||
const progress = totalCount > 0 ? loadedCount / totalCount : 0
|
||||
visualStore.setLoadingProgress('🌍 Loading from Speckle', progress)
|
||||
|
||||
// Log progress every 100 objects
|
||||
if (loadedCount % 100 === 0) {
|
||||
console.log(`Loaded ${loadedCount}/${totalCount} objects`)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Downloaded ${objects.length} objects using ObjectLoader v1`)
|
||||
visualStore.setLoadingProgress('Download complete', 1)
|
||||
|
||||
return objects
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading objects:', error)
|
||||
throw error
|
||||
} finally {
|
||||
// ObjectLoader v1 cleanup
|
||||
if (loader.dispose) {
|
||||
loader.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async downloadFromVersionId(versionId: string): Promise<SpeckleObject[]> {
|
||||
// For version IDs, we can't avoid GraphQL entirely as we need to resolve the referenced object
|
||||
// However, this method might not be used if we're getting object IDs directly from the data connector
|
||||
throw new Error('Version ID downloads not supported with weak tokens. Use object IDs directly.')
|
||||
}
|
||||
|
||||
async downloadMultipleModels(objectIds: string[]): Promise<SpeckleObject[][]> {
|
||||
const allObjects: SpeckleObject[][] = []
|
||||
|
||||
for (const objectId of objectIds) {
|
||||
const objects = await this.downloadObjectsWithChildren(objectId)
|
||||
allObjects.push(objects)
|
||||
}
|
||||
|
||||
return allObjects
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { formattingSettings as fs } from 'powerbi-visuals-utils-formattingmodel'
|
||||
|
||||
export class DataLoadingSettings extends fs.SimpleCard {
|
||||
name = 'dataLoading'
|
||||
displayName = 'Data Management'
|
||||
|
||||
public internalizeData = new fs.ToggleSwitch({
|
||||
name: 'internalizeData',
|
||||
displayName: 'Internalize Data',
|
||||
description: 'When enabled, objects are downloaded and stored in the Power BI file for offline access. When disabled, objects are loaded directly from Speckle servers (online mode).',
|
||||
value: false
|
||||
})
|
||||
|
||||
slices: fs.Slice[] = [this.internalizeData]
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
import { formattingSettings as fs } from 'powerbi-visuals-utils-formattingmodel'
|
||||
import { ColorSelectorSettings, ColorSettings } from 'src/settings/colorSettings'
|
||||
import { ColorSettings } from 'src/settings/colorSettings'
|
||||
import { CameraSettings } from 'src/settings/cameraSettings'
|
||||
import { LightingSettings } from 'src/settings/lightingSettings'
|
||||
import { DataLoadingSettings } from 'src/settings/dataLoadingSettings'
|
||||
|
||||
export class SpeckleVisualSettingsModel extends fs.Model {
|
||||
// Building my visual formatting settings card
|
||||
public color: ColorSettings = new ColorSettings()
|
||||
|
||||
public colorSelector: ColorSelectorSettings = new ColorSelectorSettings()
|
||||
public dataLoading: DataLoadingSettings = new DataLoadingSettings()
|
||||
|
||||
// public camera: CameraSettings = new CameraSettings()
|
||||
|
||||
// public lighting: LightingSettings = new LightingSettings()
|
||||
|
||||
cards = [this.color]
|
||||
cards = [this.color, this.dataLoading]
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { Version } from '@src/composables/useUpdateConnector'
|
||||
import { ColorBy, IViewerEvents } from '@src/plugins/viewer'
|
||||
import { SpeckleVisualSettingsModel } from '@src/settings/visualSettingsModel'
|
||||
import { SpeckleDataInput } from '@src/types'
|
||||
import { zipModelObjects } from '@src/utils/compression'
|
||||
import { ReceiveInfo } from '@src/utils/matrixViewUtils'
|
||||
import { zipModelObjects } from '@src/utils/compression'
|
||||
import { defineStore } from 'pinia'
|
||||
import { Vector3 } from 'three'
|
||||
import { computed, ref, shallowRef } from 'vue'
|
||||
@@ -26,7 +26,10 @@ export const useVisualStore = defineStore('visualStore', () => {
|
||||
const host = shallowRef<powerbi.extensibility.visual.IVisualHost>()
|
||||
const formattingSettings = ref<SpeckleVisualSettingsModel>()
|
||||
const loadingProgress = ref<LoadingProgress>(undefined)
|
||||
const objectsFromStore = ref<object[]>(undefined)
|
||||
const objectsFromStore = ref<object[][]>(undefined)
|
||||
|
||||
// State tracking for toggle reset prevention
|
||||
const previousToggleState = ref<boolean | undefined>(undefined)
|
||||
|
||||
const postFileSaveSkipNeeded = ref<boolean>(false)
|
||||
const postClickSkipNeeded = ref<boolean>(false)
|
||||
@@ -83,7 +86,19 @@ export const useVisualStore = defineStore('visualStore', () => {
|
||||
host.value = hostToSet
|
||||
}
|
||||
|
||||
const setReceiveInfo = (newReceiveInfo: ReceiveInfo) => (receiveInfo.value = newReceiveInfo)
|
||||
const setReceiveInfo = (newReceiveInfo: ReceiveInfo) => {
|
||||
receiveInfo.value = newReceiveInfo
|
||||
|
||||
// Always save receiveInfo to file for credentials persistence (contains token and metadata)
|
||||
// This ensures weak tokens are available even when desktop service is unavailable
|
||||
if (formattingSettings.value?.dataLoading.internalizeData.value && objectsFromStore.value) {
|
||||
// If internalize is ON and we have objects, save both objects and receiveInfo together
|
||||
writeObjectsToFile(objectsFromStore.value)
|
||||
} else {
|
||||
// Otherwise just save receiveInfo alone (credentials only)
|
||||
writeReceiveInfoToFile()
|
||||
}
|
||||
}
|
||||
|
||||
const setLatestAvailableVersion = (version: Version | null) => {
|
||||
latestAvailableVersion.value = version
|
||||
@@ -113,7 +128,7 @@ export const useVisualStore = defineStore('visualStore', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const setObjectsFromStore = (newObjectsFromStore: object[]) => {
|
||||
const setObjectsFromStore = (newObjectsFromStore: object[][]) => {
|
||||
objectsFromStore.value = newObjectsFromStore
|
||||
}
|
||||
|
||||
@@ -134,17 +149,24 @@ export const useVisualStore = defineStore('visualStore', () => {
|
||||
}
|
||||
|
||||
const loadObjectsFromFile = async (objects: object[][]) => {
|
||||
console.log('📁 loadObjectsFromFile called with:', objects.length, 'models')
|
||||
const savedVersionObjectId = objects.map((o) => (o[0] as SpeckleObject).id).join(',')
|
||||
lastLoadedRootObjectId.value = savedVersionObjectId
|
||||
viewerReloadNeeded.value = false
|
||||
console.log(`📦 Loading viewer from cached data with ${lastLoadedRootObjectId.value} id.`)
|
||||
console.log('📁 About to call viewerEmit loadObjects...')
|
||||
await viewerEmit.value('loadObjects', objects)
|
||||
console.log('📁 viewerEmit loadObjects completed')
|
||||
objectsFromStore.value = objects
|
||||
isViewerObjectsLoaded.value = true
|
||||
viewerReloadNeeded.value = false
|
||||
setIsLoadingFromFile(false)
|
||||
console.log('📁 loadObjectsFromFile completed successfully')
|
||||
}
|
||||
|
||||
const setIsLoadingFromFile = (newValue: boolean) => (isLoadingFromFile.value = newValue)
|
||||
|
||||
|
||||
/**
|
||||
* Sets upcoming data input into store to be able to pass it through viewer by evaluating the data.
|
||||
* @param newValue new data input that user dragged and dropped to the fields in visual
|
||||
@@ -159,8 +181,14 @@ export const useVisualStore = defineStore('visualStore', () => {
|
||||
await viewerEmit.value('loadObjects', dataInput.value.modelObjects)
|
||||
viewerReloadNeeded.value = false
|
||||
isViewerObjectsLoaded.value = true
|
||||
setLoadingProgress('Storing objects into file', null)
|
||||
writeObjectsToFile(dataInput.value.modelObjects)
|
||||
|
||||
// Store the model objects for potential internalization
|
||||
if (dataInput.value.modelObjects && dataInput.value.modelObjects.length > 0) {
|
||||
console.log('📦 Storing modelObjects in visualStore for internalization:', dataInput.value.modelObjects.length, 'models')
|
||||
objectsFromStore.value = dataInput.value.modelObjects
|
||||
}
|
||||
|
||||
// Note: Object internalization is now handled by toggle in visual.ts
|
||||
loadingProgress.value = undefined
|
||||
}
|
||||
|
||||
@@ -192,6 +220,23 @@ export const useVisualStore = defineStore('visualStore', () => {
|
||||
objectName: 'storedData',
|
||||
properties: {
|
||||
speckleObjects: compressedChunks,
|
||||
receiveInfo: JSON.stringify(receiveInfo.value) // Keep receiveInfo in sync when storing objects
|
||||
},
|
||||
selector: null
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
const writeReceiveInfoToFile = () => {
|
||||
// NOTE: need skipping the update function, it resets the viewer state unneccessarily.
|
||||
postFileSaveSkipNeeded.value = true
|
||||
|
||||
host.value.persistProperties({
|
||||
merge: [
|
||||
{
|
||||
objectName: 'storedData',
|
||||
properties: {
|
||||
receiveInfo: JSON.stringify(receiveInfo.value)
|
||||
},
|
||||
selector: null
|
||||
@@ -312,6 +357,22 @@ export const useVisualStore = defineStore('visualStore', () => {
|
||||
})
|
||||
}
|
||||
|
||||
const writeDataLoadingModeToFile = (internalizeData: boolean) => {
|
||||
// NOTE: need skipping the update function, it resets the viewer state unneccessarily.
|
||||
postFileSaveSkipNeeded.value = true
|
||||
host.value.persistProperties({
|
||||
merge: [
|
||||
{
|
||||
objectName: 'dataLoading',
|
||||
properties: {
|
||||
internalizeData: internalizeData
|
||||
},
|
||||
selector: null
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
const writeCameraPositionToFile = (position: Vector3, target: Vector3) => {
|
||||
// NOTE: need skipping the update function, it resets the viewer state unneccessarily.
|
||||
postFileSaveSkipNeeded.value = true
|
||||
@@ -338,7 +399,6 @@ export const useVisualStore = defineStore('visualStore', () => {
|
||||
|
||||
const clearDataInput = () => (dataInput.value = null)
|
||||
|
||||
const setIsLoadingFromFile = (newValue: boolean) => (isLoadingFromFile.value = newValue)
|
||||
|
||||
const setViewerReadyToLoad = (newValue: boolean) => (isViewerReadyToLoad.value = newValue)
|
||||
|
||||
@@ -437,6 +497,11 @@ export const useVisualStore = defineStore('visualStore', () => {
|
||||
host.value.refreshHostData()
|
||||
}
|
||||
|
||||
// Toggle state tracking functions
|
||||
const setPreviousToggleState = (state: boolean) => {
|
||||
previousToggleState.value = state
|
||||
}
|
||||
|
||||
return {
|
||||
host,
|
||||
receiveInfo,
|
||||
@@ -468,6 +533,7 @@ export const useVisualStore = defineStore('visualStore', () => {
|
||||
latestAvailableVersion,
|
||||
isConnectorUpToDate,
|
||||
commonError,
|
||||
previousToggleState,
|
||||
setCommonError,
|
||||
setLatestAvailableVersion,
|
||||
setIsOrthoProjection,
|
||||
@@ -495,6 +561,7 @@ export const useVisualStore = defineStore('visualStore', () => {
|
||||
writeCameraPositionToFile,
|
||||
writeHideBrandingToFile,
|
||||
writeNavbarVisibilityToFile,
|
||||
writeDataLoadingModeToFile,
|
||||
toggleBranding,
|
||||
toggleNavbar,
|
||||
setViewerEmitter,
|
||||
@@ -507,6 +574,7 @@ export const useVisualStore = defineStore('visualStore', () => {
|
||||
setIsLoadingFromFile,
|
||||
resetFilters,
|
||||
downloadLatestVersion,
|
||||
handleObjectsLoadedComplete
|
||||
handleObjectsLoadedComplete,
|
||||
setPreviousToggleState
|
||||
}
|
||||
})
|
||||
|
||||
@@ -11,6 +11,8 @@ import { FieldInputState, useVisualStore } from '@src/store/visualStore'
|
||||
import { delay } from 'lodash'
|
||||
import { getSlugFromHostAppNameAndVersion } from './hostAppSlug'
|
||||
import { useUpdateConnector } from '@src/composables/useUpdateConnector'
|
||||
import { SpeckleApiLoader } from '@src/loader/SpeckleApiLoader'
|
||||
import { unzipModelObjects } from './compression'
|
||||
|
||||
export class AsyncPause {
|
||||
private lastPauseTime = 0
|
||||
@@ -158,40 +160,8 @@ export type ReceiveInfo = {
|
||||
workspaceName?: string
|
||||
canHideBranding: boolean
|
||||
version?: string
|
||||
}
|
||||
|
||||
export type PreGetObjects = {
|
||||
modelExists: boolean
|
||||
objectCount?: number
|
||||
}
|
||||
|
||||
async function getPreGetObjects(commaSeparatedModelIds: string): Promise<PreGetObjects[]> {
|
||||
const modelIds = (commaSeparatedModelIds as string).split(',')
|
||||
const preGetObjects = []
|
||||
|
||||
for await (const id of modelIds) {
|
||||
const res = await getPreGetObjectsForModel(id)
|
||||
preGetObjects.push(res)
|
||||
}
|
||||
return preGetObjects
|
||||
}
|
||||
|
||||
async function getPreGetObjectsForModel(id: string): Promise<PreGetObjects> {
|
||||
try {
|
||||
const preGetObjectsRes = await fetch(`http://localhost:29364/pre-get-objects/${id}`)
|
||||
|
||||
if (!preGetObjectsRes.body) {
|
||||
console.log('No response body for pre get objects')
|
||||
return {
|
||||
modelExists: false,
|
||||
objectCount: null
|
||||
} as PreGetObjects
|
||||
}
|
||||
|
||||
return (await preGetObjectsRes.json()) as PreGetObjects
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
token: string
|
||||
projectId?: string
|
||||
}
|
||||
|
||||
async function getReceiveInfo(id) {
|
||||
@@ -206,120 +176,33 @@ async function getReceiveInfo(id) {
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
console.log("User infp couldn't retrieved from local server.")
|
||||
console.log("User info couldn't retrieved from local server.")
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchStreamedData(commaSeparatedModelIds: string, totalObjectCount: number) {
|
||||
const modelIds = (commaSeparatedModelIds as string).split(',')
|
||||
async function fetchFromSpeckleApi(
|
||||
objectIds: string,
|
||||
serverUrl: string,
|
||||
projectId: string,
|
||||
token: string
|
||||
): Promise<object[][]> {
|
||||
const ids = objectIds.split(',')
|
||||
const modelObjects = []
|
||||
|
||||
let loadedObjectCount = 0
|
||||
|
||||
for await (const id of modelIds) {
|
||||
const objects = await fetchStreamedDataForModel(id, totalObjectCount, loadedObjectCount)
|
||||
modelObjects.push(objects)
|
||||
loadedObjectCount += objects.length
|
||||
}
|
||||
return modelObjects
|
||||
}
|
||||
|
||||
async function fetchStreamedDataForModel(
|
||||
id: string,
|
||||
totalObjectCount: number,
|
||||
loadedObjectCount: number
|
||||
) {
|
||||
console.log(loadedObjectCount, totalObjectCount)
|
||||
|
||||
try {
|
||||
const visualStore = useVisualStore()
|
||||
const response = await fetch(`http://localhost:29364/get-objects/${id}`)
|
||||
|
||||
if (!response.body) {
|
||||
console.error('No response body')
|
||||
return
|
||||
}
|
||||
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
const objects = []
|
||||
let buffer = ''
|
||||
|
||||
const start = performance.now()
|
||||
console.log('Streaming started...')
|
||||
for await (const chunk of readStream(reader)) {
|
||||
// chucks.push(chuck)
|
||||
buffer += decoder.decode(chunk, { stream: true })
|
||||
|
||||
let boundary
|
||||
while ((boundary = buffer.indexOf('\n')) !== -1) {
|
||||
const jsonString = buffer.slice(0, boundary)
|
||||
buffer = buffer.slice(boundary + 1)
|
||||
|
||||
try {
|
||||
const obj = JSON.parse(jsonString)
|
||||
objects.push(obj)
|
||||
visualStore.setLoadingProgress(
|
||||
'Loading objects from storage',
|
||||
(objects.length + loadedObjectCount) / totalObjectCount
|
||||
)
|
||||
// console.log('Loading', (objects.length + loadedObjectCount) / totalObjectCount)
|
||||
|
||||
// console.log('Received object:', jsonObject)
|
||||
} catch (e) {
|
||||
console.error('Invalid JSON chunk:', jsonString)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const objectId of ids) {
|
||||
try {
|
||||
const obj = JSON.parse(buffer)
|
||||
objects.push(obj)
|
||||
// console.log('Received object:', jsonObject)
|
||||
} catch (e) {
|
||||
console.error('Invalid JSON chunk:', buffer)
|
||||
}
|
||||
|
||||
const end = performance.now()
|
||||
console.log(`Objects streamed in: ${(end - start) / 1000} s`)
|
||||
|
||||
const startObjectCleanup = performance.now()
|
||||
// Skips first element
|
||||
for (let i = 1; i < objects.length; i++) {
|
||||
const obj = objects[i]
|
||||
if (obj.speckle_type) {
|
||||
if (obj.speckle_type.includes('Objects.Data.DataObject')) {
|
||||
delete obj.properties
|
||||
}
|
||||
}
|
||||
delete obj.__closure
|
||||
}
|
||||
const endObjectCleanup = performance.now()
|
||||
console.log(`Objects cleaned up in: ${(endObjectCleanup - startObjectCleanup) / 1000} s`)
|
||||
|
||||
try {
|
||||
const sizeInBytes = new TextEncoder().encode(JSON.stringify(objects)).length
|
||||
const sizeInMB = sizeInBytes / (1024 * 1024)
|
||||
console.log(`Size of objects: ${sizeInMB} MB`)
|
||||
console.log(`Downloading from Speckle API: ${objectId}`)
|
||||
const loader = new SpeckleApiLoader(serverUrl, projectId, token)
|
||||
const objects = await loader.downloadObjectsWithChildren(objectId)
|
||||
modelObjects.push(objects)
|
||||
console.log(`Downloaded ${objects.length} objects from Speckle`)
|
||||
} catch (error) {
|
||||
console.log("Can't calculate the size of the model")
|
||||
console.log(error)
|
||||
console.error(`Failed to download objects from Speckle:`, error)
|
||||
throw error
|
||||
}
|
||||
|
||||
return objects
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
console.log("Objects couldn't retrieved from local server.")
|
||||
} finally {
|
||||
console.log('Streaming finished!')
|
||||
}
|
||||
}
|
||||
|
||||
async function* readStream(reader) {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
yield value
|
||||
}
|
||||
return modelObjects
|
||||
}
|
||||
|
||||
export async function processMatrixView(
|
||||
@@ -327,7 +210,8 @@ export async function processMatrixView(
|
||||
host: powerbi.extensibility.visual.IVisualHost,
|
||||
hasColorFilter: boolean,
|
||||
settings: SpeckleVisualSettingsModel,
|
||||
onSelectionPair: (objId: string, selectionId: powerbi.extensibility.ISelectionId) => void
|
||||
onSelectionPair: (objId: string, selectionId: powerbi.extensibility.ISelectionId) => void,
|
||||
internalizedData?: string
|
||||
): Promise<SpeckleDataInput> {
|
||||
const visualStore = useVisualStore()
|
||||
const objectIds = [],
|
||||
@@ -340,10 +224,86 @@ export async function processMatrixView(
|
||||
const localMatrixView = matrixView.rows.root.children
|
||||
let id = null
|
||||
|
||||
if (hasColorFilter) {
|
||||
id = localMatrixView[0].children[0].values[0].value as unknown as string
|
||||
} else {
|
||||
id = localMatrixView[0].values[0].value as unknown as string
|
||||
// Safety check for matrix data structure
|
||||
if (!localMatrixView || localMatrixView.length === 0) {
|
||||
throw new Error('Matrix view has no data rows')
|
||||
}
|
||||
|
||||
try {
|
||||
if (hasColorFilter) {
|
||||
if (!localMatrixView[0].children || localMatrixView[0].children.length === 0 || !localMatrixView[0].children[0].values) {
|
||||
throw new Error('Matrix view structure is incomplete for color filter mode')
|
||||
}
|
||||
id = localMatrixView[0].children[0].values[0].value as unknown as string
|
||||
} else {
|
||||
if (!localMatrixView[0].values || !localMatrixView[0].values[0]) {
|
||||
throw new Error('Matrix view structure is incomplete for normal mode')
|
||||
}
|
||||
id = localMatrixView[0].values[0].value as unknown as string
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error accessing matrix data:', error)
|
||||
throw new Error(`Failed to extract root object ID from matrix: ${error.message}`)
|
||||
}
|
||||
|
||||
// Check for internalized data but ONLY if it matches current matrix data
|
||||
let internalizedModelObjects: object[][] | undefined = undefined
|
||||
if (settings.dataLoading.internalizeData.value && internalizedData) {
|
||||
console.log('📁 Checking internalized data in processMatrixView')
|
||||
|
||||
try {
|
||||
internalizedModelObjects = unzipModelObjects(internalizedData)
|
||||
|
||||
if (internalizedModelObjects && internalizedModelObjects.length > 0) {
|
||||
// CRITICAL: Validate that internalized data matches current matrix data
|
||||
const internalizedRootId = (internalizedModelObjects[0][0] as any).id
|
||||
if (internalizedRootId !== id) {
|
||||
console.log(`📁 Internalized data mismatch: stored=${internalizedRootId}, current=${id}. Using fresh data.`)
|
||||
internalizedModelObjects = undefined // Clear internalized data - use fresh data instead
|
||||
} else {
|
||||
console.log(
|
||||
'📁 Successfully validated internalized data matches current matrix:',
|
||||
internalizedModelObjects.length,
|
||||
'models'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (internalizedModelObjects && internalizedModelObjects.length > 0) {
|
||||
|
||||
// Set dummy receiveInfo to prevent UI errors
|
||||
if (!visualStore.receiveInfo) {
|
||||
visualStore.setReceiveInfo({
|
||||
userEmail: 'offline@speckle.systems',
|
||||
serverUrl: 'offline',
|
||||
sourceApplication: 'PowerBI Offline',
|
||||
workspaceId: 'offline',
|
||||
workspaceName: 'Offline Workspace',
|
||||
workspaceLogo: '',
|
||||
version: '1.0.0',
|
||||
canHideBranding: false,
|
||||
token: 'offline',
|
||||
projectId: 'offline'
|
||||
})
|
||||
}
|
||||
|
||||
// Only reload if switching models or not already loaded
|
||||
const needsReload = !visualStore.isViewerObjectsLoaded || visualStore.lastLoadedRootObjectId !== id
|
||||
if (needsReload) {
|
||||
console.log('🔄 Forcing viewer reload for internalized data (model switch or first load)')
|
||||
visualStore.setViewerReloadNeeded()
|
||||
visualStore.setViewerReadyToLoad(true)
|
||||
visualStore.setLoadingProgress('📁 Loading from file', null)
|
||||
} else {
|
||||
console.log('📁 Internalized data already loaded, skipping reload')
|
||||
}
|
||||
visualStore.lastLoadedRootObjectId = id // Set to current ID to skip API calls
|
||||
} else {
|
||||
console.error('📁 Failed to unzip internalized data')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('📁 Error processing internalized data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// const id = localMatrixView[0].values[0].value as unknown as string
|
||||
@@ -352,48 +312,85 @@ export async function processMatrixView(
|
||||
|
||||
let modelObjects: object[][] = undefined
|
||||
|
||||
if (visualStore.isLoadingFromFile) {
|
||||
console.log('The data is loading from file, skipping the streaming it.')
|
||||
}
|
||||
|
||||
if (visualStore.lastLoadedRootObjectId !== id && !visualStore.isLoadingFromFile) {
|
||||
if (
|
||||
visualStore.lastLoadedRootObjectId !== id &&
|
||||
!visualStore.isLoadingFromFile &&
|
||||
!internalizedModelObjects
|
||||
) {
|
||||
const start = performance.now()
|
||||
|
||||
const getPreGetObjectsRes: PreGetObjects[] = await getPreGetObjects(id)
|
||||
|
||||
if (getPreGetObjectsRes.some((preGetObjects) => preGetObjects.modelExists === false)) {
|
||||
visualStore.setCommonError(
|
||||
'Version Object ID is not found in storage. Please make sure you placed correct field or consider refreshing your data via data connector.'
|
||||
)
|
||||
visualStore.setViewerReadyToLoad(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Get receive info from desktop service to populate visual store
|
||||
const receiveInfo = await getReceiveInfo(id)
|
||||
if (receiveInfo) {
|
||||
visualStore.setReceiveInfo({
|
||||
userEmail: receiveInfo.email,
|
||||
serverUrl: receiveInfo.server,
|
||||
sourceApplication: getSlugFromHostAppNameAndVersion(receiveInfo.sourceApplication),
|
||||
workspaceId: receiveInfo.workspaceId,
|
||||
workspaceName: receiveInfo.workspaceName,
|
||||
workspaceLogo: receiveInfo.workspaceLogo,
|
||||
version: receiveInfo.version,
|
||||
canHideBranding: receiveInfo.canHideBranding
|
||||
userEmail: receiveInfo.email || receiveInfo.Email,
|
||||
serverUrl: receiveInfo.server || receiveInfo.Server,
|
||||
sourceApplication: getSlugFromHostAppNameAndVersion(
|
||||
receiveInfo.sourceApplication || receiveInfo.SourceApplication
|
||||
),
|
||||
workspaceId: receiveInfo.workspaceId || receiveInfo.WorkspaceId,
|
||||
workspaceName: receiveInfo.workspaceName || receiveInfo.WorkspaceName,
|
||||
workspaceLogo: receiveInfo.workspaceLogo || receiveInfo.WorkspaceLogo,
|
||||
version: receiveInfo.version || receiveInfo.Version,
|
||||
canHideBranding: receiveInfo.canHideBranding ?? receiveInfo.CanHideBranding,
|
||||
token: receiveInfo.weakToken || receiveInfo.WeakToken,
|
||||
projectId: receiveInfo.projectId || receiveInfo.ProjectId
|
||||
})
|
||||
console.log(`Receive info retrieved from desktop service`, receiveInfo)
|
||||
console.log(`Receive info retrieved from desktop service - credentials loaded`)
|
||||
}
|
||||
|
||||
const totalObjectCount = getPreGetObjectsRes.reduce((sum, obj) => {
|
||||
return sum + (obj.objectCount ?? 0)
|
||||
}, 0)
|
||||
// Now get the data from visual store for Speckle API download
|
||||
const token = visualStore.receiveInfo?.token
|
||||
const serverUrl = visualStore.receiveInfo?.serverUrl
|
||||
const projectId = visualStore.receiveInfo?.projectId
|
||||
|
||||
if (!token || !serverUrl || !projectId) {
|
||||
visualStore.setCommonError(
|
||||
'Missing Speckle credentials. Please refresh the data from the data connector.'
|
||||
)
|
||||
visualStore.setViewerReadyToLoad(false)
|
||||
return {
|
||||
modelObjects: [],
|
||||
objectIds: [],
|
||||
selectedIds: [],
|
||||
colorByIds: null,
|
||||
objectTooltipData: new Map(),
|
||||
isFromStore: false
|
||||
}
|
||||
}
|
||||
|
||||
visualStore.setViewerReadyToLoad(true)
|
||||
// stream data
|
||||
modelObjects = await fetchStreamedData(id, totalObjectCount)
|
||||
|
||||
console.log('Downloading objects directly from Speckle API...')
|
||||
console.log(`Server: ${serverUrl}, Project: ${projectId}, Object: ${id}`)
|
||||
try {
|
||||
modelObjects = await fetchFromSpeckleApi(id, serverUrl, projectId, token)
|
||||
console.log('Successfully downloaded from Speckle API')
|
||||
|
||||
// Debug: Check what we're passing to the viewer
|
||||
if (modelObjects && modelObjects.length > 0 && modelObjects[0].length > 0) {
|
||||
console.log('ModelObjects structure:', {
|
||||
totalModels: modelObjects.length,
|
||||
firstModelObjectCount: modelObjects[0].length,
|
||||
firstObject: modelObjects[0][0]
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to download from Speckle API:', error)
|
||||
visualStore.setCommonError(`Failed to download objects from Speckle: ${error.message}`)
|
||||
visualStore.setViewerReadyToLoad(false)
|
||||
return {
|
||||
modelObjects: [],
|
||||
objectIds: [],
|
||||
selectedIds: [],
|
||||
colorByIds: null,
|
||||
objectTooltipData: new Map(),
|
||||
isFromStore: false
|
||||
}
|
||||
}
|
||||
|
||||
visualStore.setViewerReloadNeeded() // they should be marked as deferred action bc of update function complexity.
|
||||
visualStore.setLoadingProgress('Loading objects into viewer', null)
|
||||
visualStore.setLoadingProgress('🌍 Loading objects into viewer', null)
|
||||
console.log(`🚀 Upload is completed in ${(performance.now() - start) / 1000} s!`)
|
||||
}
|
||||
|
||||
@@ -538,11 +535,11 @@ export async function processMatrixView(
|
||||
previousPalette = host.colorPalette['colorPalette']
|
||||
|
||||
return {
|
||||
modelObjects,
|
||||
modelObjects: internalizedModelObjects || modelObjects, // Use internalized data if available
|
||||
objectIds,
|
||||
selectedIds,
|
||||
colorByIds: colorByIds.length > 0 ? colorByIds : null,
|
||||
objectTooltipData,
|
||||
isFromStore: false
|
||||
isFromStore: !!internalizedModelObjects // true if loaded from internalized data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { selectionHandlerKey, tooltipHandlerKey } from 'src/injectionKeys'
|
||||
import { SpeckleDataInput } from './types'
|
||||
import { processMatrixView, ReceiveInfo, validateMatrixView } from './utils/matrixViewUtils'
|
||||
import { SpeckleVisualSettingsModel } from './settings/visualSettingsModel'
|
||||
import { unzipModelObjects } from './utils/compression'
|
||||
|
||||
import TooltipHandler from './handlers/tooltipHandler'
|
||||
import SelectionHandler from './handlers/selectionHandler'
|
||||
@@ -21,7 +22,6 @@ import ITooltipService = powerbi.extensibility.ITooltipService
|
||||
|
||||
import { pinia } from './plugins/pinia'
|
||||
import { useVisualStore } from './store/visualStore'
|
||||
import { unzipModelObjects } from './utils/compression'
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
export class Visual implements IVisual {
|
||||
@@ -88,13 +88,43 @@ export class Visual implements IVisual {
|
||||
|
||||
// @ts-ignore
|
||||
console.log('⤴️ Update type 👉', powerbi.VisualUpdateType[options.type])
|
||||
|
||||
this.formattingSettings = this.formattingSettingsService.populateFormattingSettingsModel(
|
||||
SpeckleVisualSettingsModel,
|
||||
options.dataViews[0]
|
||||
)
|
||||
|
||||
visualStore.setFormattingSettings(this.formattingSettings)
|
||||
console.log('Selector colors', this.formattingSettings.colorSelector)
|
||||
console.log(
|
||||
'Data Loading - Internalize Data:',
|
||||
this.formattingSettings.dataLoading.internalizeData.value
|
||||
)
|
||||
|
||||
// Handle toggle state changes
|
||||
const currentToggleState = this.formattingSettings.dataLoading.internalizeData.value
|
||||
const previousToggleState = visualStore.previousToggleState
|
||||
|
||||
// Detect user toggle changes
|
||||
if (previousToggleState !== undefined && currentToggleState !== previousToggleState) {
|
||||
console.log('🔄 User changed toggle from', previousToggleState, 'to', currentToggleState)
|
||||
|
||||
if (currentToggleState) {
|
||||
// Toggle switched ON - internalize via streaming
|
||||
if (visualStore.isViewerObjectsLoaded && visualStore.lastLoadedRootObjectId) {
|
||||
console.log('📁 Toggle ON - starting internalization')
|
||||
await this.internalizeCurrentViewerData()
|
||||
} else {
|
||||
console.log('📁 Toggle ON - no active session to internalize')
|
||||
}
|
||||
} else {
|
||||
// Toggle switched OFF - remove internalized data
|
||||
console.log('🗑️ Toggle OFF - removing internalized data')
|
||||
this.removeInternalizedData()
|
||||
}
|
||||
}
|
||||
|
||||
// CRITICAL: Always update the previous state for next comparison
|
||||
visualStore.setPreviousToggleState(currentToggleState)
|
||||
|
||||
try {
|
||||
const matrixView = options.dataViews[0].matrix
|
||||
@@ -114,16 +144,10 @@ export class Visual implements IVisual {
|
||||
return
|
||||
case powerbi.VisualUpdateType.Data:
|
||||
try {
|
||||
// read saved data from file if any
|
||||
if (
|
||||
!visualStore.isViewerObjectsLoaded &&
|
||||
this.isFirstViewerLoad &&
|
||||
options.dataViews[0].metadata.objects
|
||||
) {
|
||||
const chunks = options.dataViews[0].metadata.objects.storedData
|
||||
?.speckleObjects as string
|
||||
const objectsFromFile = unzipModelObjects(chunks)
|
||||
// read saved settings from file if any
|
||||
console.log('🔍 Checking for other saved settings:')
|
||||
|
||||
if (!visualStore.isViewerObjectsLoaded && options.dataViews[0].metadata.objects) {
|
||||
if (options.dataViews[0].metadata.objects.viewMode?.defaultViewMode as string) {
|
||||
console.log(
|
||||
`Default View Mode: ${
|
||||
@@ -198,7 +222,9 @@ export class Visual implements IVisual {
|
||||
|
||||
if (camera && 'zoomOnFilter' in camera) {
|
||||
console.log(
|
||||
`Zoom on filter?: ${options.dataViews[0].metadata.objects.camera?.zoomOnFilter as boolean}`
|
||||
`Zoom on filter?: ${
|
||||
options.dataViews[0].metadata.objects.camera?.zoomOnFilter as boolean
|
||||
}`
|
||||
)
|
||||
|
||||
visualStore.setIsZoomOnFilterActive(
|
||||
@@ -206,31 +232,59 @@ export class Visual implements IVisual {
|
||||
)
|
||||
}
|
||||
|
||||
// get receive info from file for mixpanel
|
||||
// Log persisted data loading setting but don't force sync
|
||||
if (
|
||||
options.dataViews[0].metadata.objects.dataLoading?.internalizeData !== undefined
|
||||
) {
|
||||
console.log(
|
||||
`Stored Data Loading - Internalize Data: ${
|
||||
options.dataViews[0].metadata.objects.dataLoading?.internalizeData as boolean
|
||||
}`
|
||||
)
|
||||
}
|
||||
|
||||
// get receive info from file for persistence
|
||||
try {
|
||||
const receiveInfoFromFile = JSON.parse(
|
||||
options.dataViews[0].metadata.objects.storedData?.receiveInfo as string
|
||||
) as ReceiveInfo
|
||||
visualStore.setReceiveInfo(receiveInfoFromFile)
|
||||
// Don't call setReceiveInfo here as it would trigger another save
|
||||
visualStore.receiveInfo = receiveInfoFromFile
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
console.log('missing mixpanel info')
|
||||
}
|
||||
|
||||
const savedVersionObjectId = objectsFromFile.map((o) => o[0].id).join(',')
|
||||
if (visualStore.lastLoadedRootObjectId !== savedVersionObjectId) {
|
||||
this.tryReadFromFile(objectsFromFile, visualStore)
|
||||
console.log('missing stored receive info')
|
||||
}
|
||||
}
|
||||
|
||||
// Check for internalized data
|
||||
const internalizedData = options.dataViews[0].metadata.objects?.storedData
|
||||
?.speckleObjects as string
|
||||
|
||||
const input = await processMatrixView(
|
||||
matrixView,
|
||||
this.host,
|
||||
validationResult.colorBy,
|
||||
this.formattingSettings,
|
||||
(obj, id) => this.selectionHandler.set(obj, id)
|
||||
(obj, id) => this.selectionHandler.set(obj, id),
|
||||
internalizedData
|
||||
)
|
||||
this.updateViewer(input)
|
||||
|
||||
// Auto-internalize new API data if toggle is ON and this is fresh data (not from store)
|
||||
// Imagine that user has a visual and select internalizing data and changes the data source
|
||||
// This will automatically internalize the new data
|
||||
if (
|
||||
this.formattingSettings.dataLoading.internalizeData.value &&
|
||||
input.modelObjects &&
|
||||
input.modelObjects.length > 0 &&
|
||||
!input.isFromStore
|
||||
) {
|
||||
console.log('📦 Auto-internalizing new API data since toggle is ON')
|
||||
// Trigger internalization after objects are loaded
|
||||
setTimeout(() => {
|
||||
this.internalizeCurrentViewerData()
|
||||
}, 2000) // avoid a race condition (i know)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Data update error', error ?? 'Unknown')
|
||||
}
|
||||
@@ -258,9 +312,8 @@ export class Visual implements IVisual {
|
||||
}
|
||||
|
||||
public getFormattingModel(): powerbi.visuals.FormattingModel {
|
||||
console.log('Showing Formatting settings', this.formattingSettings)
|
||||
console.log('🎨 getFormattingModel called')
|
||||
const model = this.formattingSettingsService.buildFormattingModel(this.formattingSettings)
|
||||
console.log('Formatting model was created', model)
|
||||
return model
|
||||
}
|
||||
|
||||
@@ -276,14 +329,13 @@ export class Visual implements IVisual {
|
||||
// we should give some time to Vue to render ViewerWrapper component to be able to have proper emitter setup. Happiness level 6/10
|
||||
setTimeout(() => {
|
||||
visualStore.setDataInput(input)
|
||||
// visualStore.writeObjectsToFile(input.objects)
|
||||
}, 250)
|
||||
}
|
||||
}
|
||||
|
||||
private tryReadFromFile(objectsFromFile: object[][], visualStore) {
|
||||
visualStore.setViewerReadyToLoad(true)
|
||||
visualStore.setIsLoadingFromFile(true) // to block unnecessary streaming data if bg service is running
|
||||
visualStore.setIsLoadingFromFile(true)
|
||||
setTimeout(() => {
|
||||
visualStore.loadObjectsFromFile(objectsFromFile)
|
||||
this.isFirstViewerLoad = false
|
||||
@@ -291,6 +343,137 @@ export class Visual implements IVisual {
|
||||
console.log(`${objectsFromFile.length} objects retrieved from persistent properties!`)
|
||||
}
|
||||
|
||||
private async internalizeCurrentViewerData() {
|
||||
const visualStore = useVisualStore()
|
||||
|
||||
// Get the current root object ID from the last loaded data
|
||||
if (!visualStore.lastLoadedRootObjectId) {
|
||||
console.log('📁 No root object ID to internalize')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('📁 Starting internalization via desktop service streaming...')
|
||||
|
||||
visualStore.setLoadingProgress('📦 Internalizing data...', null)
|
||||
|
||||
// Use desktop service for internalization
|
||||
// TBD: getting objects from viewer caused two issue:
|
||||
// - Data format -> we need to make an extra operation to match with the offline loader
|
||||
// - Memory -> need to save data two times so sometimes causes memory issues
|
||||
const rootObjectIds = visualStore.lastLoadedRootObjectId
|
||||
const projectId = visualStore.receiveInfo?.projectId
|
||||
|
||||
// Handle federated models by processing each object ID separately
|
||||
const objectIds = rootObjectIds.split(',')
|
||||
let allStreamedObjects = []
|
||||
|
||||
for (const objectId of objectIds) {
|
||||
console.log(`📁 Fetching objects for ID: ${objectId}`)
|
||||
|
||||
// For federated models, pass project ID explicitly to avoid "project id is not set" error
|
||||
const url = projectId
|
||||
? `http://localhost:29364/get-objects/${objectId}?projectId=${projectId}`
|
||||
: `http://localhost:29364/get-objects/${objectId}`
|
||||
|
||||
const response = await fetch(url)
|
||||
|
||||
if (!response.body) {
|
||||
console.error(`📁 No response body from desktop service for ${objectId}`)
|
||||
continue
|
||||
}
|
||||
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let allObjectsData = ''
|
||||
|
||||
console.log(`📁 Streaming objects from desktop service for ${objectId}...`)
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
allObjectsData += decoder.decode(value, { stream: true })
|
||||
}
|
||||
|
||||
// Parse NDJSON (newline-delimited JSON) format
|
||||
const lines = allObjectsData.trim().split('\n')
|
||||
const objectsForThisId = lines.map((line) => JSON.parse(line))
|
||||
|
||||
console.log(`📁 Streamed ${objectsForThisId.length} objects for ID ${objectId}`)
|
||||
allStreamedObjects.push(...objectsForThisId)
|
||||
}
|
||||
|
||||
const streamedObjects = allStreamedObjects
|
||||
|
||||
if (streamedObjects.length === 0) {
|
||||
console.error('📁 No objects retrieved from desktop service')
|
||||
visualStore.clearLoadingProgress()
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`📁 Retrieved ${streamedObjects.length} total objects from desktop service`)
|
||||
|
||||
// Clean up objects to reduce file size (same as desktop service does)
|
||||
const cleanedObjects = streamedObjects.map((obj: any, index: number) => {
|
||||
// Skip first object (root), clean others
|
||||
if (index === 0) return obj
|
||||
|
||||
const cleanedObj = { ...obj }
|
||||
|
||||
// Remove unnecessary properties
|
||||
if (cleanedObj.speckle_type?.includes('Objects.Data.DataObject')) {
|
||||
delete cleanedObj.properties
|
||||
}
|
||||
delete cleanedObj.__closure
|
||||
|
||||
return cleanedObj
|
||||
})
|
||||
|
||||
console.log(`📁 Cleaned objects: ${cleanedObjects.length} total`)
|
||||
|
||||
// Wrap in array format expected by viewer (object[][])
|
||||
const modelObjectsArray = [cleanedObjects]
|
||||
|
||||
// Use existing writeObjectsToFile method from visualStore
|
||||
visualStore.writeObjectsToFile(modelObjectsArray)
|
||||
|
||||
// Clear loading message immediately when done
|
||||
visualStore.clearLoadingProgress()
|
||||
|
||||
console.log('📁 Successfully internalized data via desktop service!')
|
||||
} catch (error) {
|
||||
console.error('📁 Failed to internalize via desktop service:', error)
|
||||
|
||||
// Clear loading message immediately on error
|
||||
visualStore.clearLoadingProgress()
|
||||
}
|
||||
}
|
||||
|
||||
private removeInternalizedData() {
|
||||
const visualStore = useVisualStore()
|
||||
|
||||
try {
|
||||
// Clear stored data from PowerBI file
|
||||
this.host.persistProperties({
|
||||
merge: [
|
||||
{
|
||||
objectName: 'storedData',
|
||||
properties: {
|
||||
speckleObjects: null,
|
||||
receiveInfo: null
|
||||
},
|
||||
selector: null
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
console.log('🗑️ Successfully removed internalized data from file!')
|
||||
} catch (error) {
|
||||
console.error('🗑️ Failed to remove internalized data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
public async destroy() {
|
||||
await this.clear()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user