feat(fe2): enable large file uploads (#4965)

This commit is contained in:
Kristaps Fabians Geikins
2025-06-20 10:50:16 +03:00
committed by GitHub
parent 3c1a10bff1
commit 3b641024cc
12 changed files with 218 additions and 39 deletions
@@ -277,7 +277,7 @@ const onModelCreate = (params: { model: ProjectPageLatestItemsModelItemFragment
if (!isFileUploadUploadable.value) return
uploadSelected({
modelName: params.model.name
model: params.model
})
}
@@ -296,11 +296,18 @@ watch(showNewModelDialog, (newVal, oldVal) => {
watch(isUploading, (newVal, oldVal) => {
// fileUpload is always gonna be non-null when isUploading changes
emit('uploading', { isUploading: newVal, upload: fileUpload.value! })
emit('uploading', {
isUploading: newVal,
upload: fileUpload.value!,
error: errorMessage.value
})
if (!newVal && oldVal) {
// Reset file upload state when upload finishes
resetSelected()
// but only if it was successful! otherwise we wanna show the error
if (!errorMessage.value) {
resetSelected()
}
}
})
@@ -238,7 +238,7 @@ const onCardClick = (event: KeyboardEvent | MouseEvent) => {
}
const onVersionUploading = (payload: FileAreaUploadingPayload) => {
isVersionUploading.value = payload.isUploading
isVersionUploading.value = !!(payload.isUploading || payload.error)
}
const triggerVersionUpload = () => {
@@ -204,7 +204,7 @@ const calculateLoaderId = () => {
}
const onModelUploading = (payload: FileAreaUploadingPayload) => {
isModelUploading.value = payload.isUploading
isModelUploading.value = !!(payload.isUploading || payload.error)
}
watch(areQueriesLoading, (newVal) => {
@@ -216,7 +216,7 @@ const calculateLoaderId = () => {
}
const onModelUploading = (payload: FileAreaUploadingPayload) => {
isModelUploading.value = payload.isUploading
isModelUploading.value = !!(payload.isUploading || payload.error)
}
watch(areQueriesLoading, (newVal) => {
@@ -436,7 +436,7 @@ const triggerVersionUpload = () => {
}
const onVersionUploading = (payload: FileAreaUploadingPayload) => {
isVersionUploading.value = payload.isUploading
isVersionUploading.value = !!(payload.isUploading || payload.error)
}
const onVersionsClick = () => {
@@ -202,6 +202,6 @@ const gridClasses = computed(() => [
])
const onModelUploading = (payload: FileAreaUploadingPayload) => {
isModelUploading.value = payload.isUploading
isModelUploading.value = !!(payload.isUploading || payload.error)
}
</script>
@@ -211,6 +211,8 @@ type Documents = {
"\n query ServerInfoBlobSizeLimit {\n serverInfo {\n configuration {\n blobSizeLimitBytes\n }\n }\n }\n": typeof types.ServerInfoBlobSizeLimitDocument,
"\n query ServerInfoAllScopes {\n serverInfo {\n scopes {\n name\n description\n }\n }\n }\n": typeof types.ServerInfoAllScopesDocument,
"\n query ProjectModelsSelectorValues($projectId: String!, $cursor: String) {\n project(id: $projectId) {\n id\n models(limit: 100, cursor: $cursor) {\n cursor\n totalCount\n items {\n ...CommonModelSelectorModel\n }\n }\n }\n }\n": typeof types.ProjectModelsSelectorValuesDocument,
"\n mutation GenerateUploadUrl($input: GenerateFileUploadUrlInput!) {\n fileUploadMutations {\n generateUploadUrl(input: $input) {\n url\n fileId\n }\n }\n }\n": typeof types.GenerateUploadUrlDocument,
"\n mutation StartFileImport($input: StartFileImportInput!) {\n fileUploadMutations {\n startFileImport(input: $input) {\n id\n }\n }\n }\n": typeof types.StartFileImportDocument,
"\n fragment UseFileImport_Project on Project {\n id\n }\n": typeof types.UseFileImport_ProjectFragmentDoc,
"\n fragment UseFileImport_Model on Model {\n id\n name\n }\n": typeof types.UseFileImport_ModelFragmentDoc,
"\n query MainServerInfoData {\n serverInfo {\n adminContact\n canonicalUrl\n company\n description\n guestModeEnabled\n inviteOnly\n name\n termsOfService\n version\n automateUrl\n configuration {\n isEmailEnabled\n }\n }\n }\n": typeof types.MainServerInfoDataDocument,
@@ -662,6 +664,8 @@ const documents: Documents = {
"\n query ServerInfoBlobSizeLimit {\n serverInfo {\n configuration {\n blobSizeLimitBytes\n }\n }\n }\n": types.ServerInfoBlobSizeLimitDocument,
"\n query ServerInfoAllScopes {\n serverInfo {\n scopes {\n name\n description\n }\n }\n }\n": types.ServerInfoAllScopesDocument,
"\n query ProjectModelsSelectorValues($projectId: String!, $cursor: String) {\n project(id: $projectId) {\n id\n models(limit: 100, cursor: $cursor) {\n cursor\n totalCount\n items {\n ...CommonModelSelectorModel\n }\n }\n }\n }\n": types.ProjectModelsSelectorValuesDocument,
"\n mutation GenerateUploadUrl($input: GenerateFileUploadUrlInput!) {\n fileUploadMutations {\n generateUploadUrl(input: $input) {\n url\n fileId\n }\n }\n }\n": types.GenerateUploadUrlDocument,
"\n mutation StartFileImport($input: StartFileImportInput!) {\n fileUploadMutations {\n startFileImport(input: $input) {\n id\n }\n }\n }\n": types.StartFileImportDocument,
"\n fragment UseFileImport_Project on Project {\n id\n }\n": types.UseFileImport_ProjectFragmentDoc,
"\n fragment UseFileImport_Model on Model {\n id\n name\n }\n": types.UseFileImport_ModelFragmentDoc,
"\n query MainServerInfoData {\n serverInfo {\n adminContact\n canonicalUrl\n company\n description\n guestModeEnabled\n inviteOnly\n name\n termsOfService\n version\n automateUrl\n configuration {\n isEmailEnabled\n }\n }\n }\n": types.MainServerInfoDataDocument,
@@ -1718,6 +1722,14 @@ export function graphql(source: "\n query ServerInfoAllScopes {\n serverInfo
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query ProjectModelsSelectorValues($projectId: String!, $cursor: String) {\n project(id: $projectId) {\n id\n models(limit: 100, cursor: $cursor) {\n cursor\n totalCount\n items {\n ...CommonModelSelectorModel\n }\n }\n }\n }\n"): (typeof documents)["\n query ProjectModelsSelectorValues($projectId: String!, $cursor: String) {\n project(id: $projectId) {\n id\n models(limit: 100, cursor: $cursor) {\n cursor\n totalCount\n items {\n ...CommonModelSelectorModel\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation GenerateUploadUrl($input: GenerateFileUploadUrlInput!) {\n fileUploadMutations {\n generateUploadUrl(input: $input) {\n url\n fileId\n }\n }\n }\n"): (typeof documents)["\n mutation GenerateUploadUrl($input: GenerateFileUploadUrlInput!) {\n fileUploadMutations {\n generateUploadUrl(input: $input) {\n url\n fileId\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation StartFileImport($input: StartFileImportInput!) {\n fileUploadMutations {\n startFileImport(input: $input) {\n id\n }\n }\n }\n"): (typeof documents)["\n mutation StartFileImport($input: StartFileImportInput!) {\n fileUploadMutations {\n startFileImport(input: $input) {\n id\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -5800,6 +5800,20 @@ export type ProjectModelsSelectorValuesQueryVariables = Exact<{
export type ProjectModelsSelectorValuesQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, models: { __typename?: 'ModelCollection', cursor?: string | null, totalCount: number, items: Array<{ __typename?: 'Model', id: string, name: string }> } } };
export type GenerateUploadUrlMutationVariables = Exact<{
input: GenerateFileUploadUrlInput;
}>;
export type GenerateUploadUrlMutation = { __typename?: 'Mutation', fileUploadMutations: { __typename?: 'FileUploadMutations', generateUploadUrl: { __typename?: 'GenerateFileUploadUrlOutput', url: string, fileId: string } } };
export type StartFileImportMutationVariables = Exact<{
input: StartFileImportInput;
}>;
export type StartFileImportMutation = { __typename?: 'Mutation', fileUploadMutations: { __typename?: 'FileUploadMutations', startFileImport: { __typename?: 'FileUpload', id: string } } };
export type UseFileImport_ProjectFragment = { __typename?: 'Project', id: string };
export type UseFileImport_ModelFragment = { __typename?: 'Model', id: string, name: string };
@@ -7609,6 +7623,8 @@ export const MentionsUserSearchDocument = {"kind":"Document","definitions":[{"ki
export const ServerInfoBlobSizeLimitDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ServerInfoBlobSizeLimit"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configuration"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"blobSizeLimitBytes"}}]}}]}}]}}]} as unknown as DocumentNode<ServerInfoBlobSizeLimitQuery, ServerInfoBlobSizeLimitQueryVariables>;
export const ServerInfoAllScopesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ServerInfoAllScopes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"scopes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]}}]}}]} as unknown as DocumentNode<ServerInfoAllScopesQuery, ServerInfoAllScopesQueryVariables>;
export const ProjectModelsSelectorValuesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectModelsSelectorValues"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"models"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"100"}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CommonModelSelectorModel"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CommonModelSelectorModel"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Model"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]} as unknown as DocumentNode<ProjectModelsSelectorValuesQuery, ProjectModelsSelectorValuesQueryVariables>;
export const GenerateUploadUrlDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"GenerateUploadUrl"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"GenerateFileUploadUrlInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fileUploadMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"generateUploadUrl"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"fileId"}}]}}]}}]}}]} as unknown as DocumentNode<GenerateUploadUrlMutation, GenerateUploadUrlMutationVariables>;
export const StartFileImportDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"StartFileImport"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"StartFileImportInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fileUploadMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"startFileImport"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode<StartFileImportMutation, StartFileImportMutationVariables>;
export const MainServerInfoDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MainServerInfoData"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"adminContact"}},{"kind":"Field","name":{"kind":"Name","value":"canonicalUrl"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"guestModeEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"inviteOnly"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"termsOfService"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"automateUrl"}},{"kind":"Field","name":{"kind":"Name","value":"configuration"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"isEmailEnabled"}}]}}]}}]}}]} as unknown as DocumentNode<MainServerInfoDataQuery, MainServerInfoDataQueryVariables>;
export const DeleteAccessTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteAccessToken"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"token"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apiTokenRevoke"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}}]}]}}]} as unknown as DocumentNode<DeleteAccessTokenMutation, DeleteAccessTokenMutationVariables>;
export const CreateAccessTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateAccessToken"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"token"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ApiTokenCreateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apiTokenCreate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}}]}]}}]} as unknown as DocumentNode<CreateAccessTokenMutation, CreateAccessTokenMutationVariables>;
+10 -3
View File
@@ -8,18 +8,25 @@ export enum FileUploadConvertedStatus {
Error = 3
}
export function importFile(
export type ImportFile = (
params: {
file: File
projectId: string
apiOrigin: string
authToken: string
modelName?: string
modelName: string
modelId: string
},
callbacks?: Partial<{
onProgress: (percentage: number) => void
}>
) {
) => Promise<BlobPostResultItem>
/**
* Old upload mechanism that streams uploads through the server
* @deprecated Use useFileImportApi() instead
*/
export const importFileLegacy: ImportFile = (params, callbacks) => {
const { file, projectId, modelName, apiOrigin, authToken } = params
const { onProgress } = callbacks || {}
@@ -1,20 +1,149 @@
import type { MaybeRef } from '@vueuse/core'
import { ensureError } from '@speckle/shared'
import { buildManualPromise, ensureError } from '@speckle/shared'
import type { MaybeNullOrUndefined, Nullable, Optional } from '@speckle/shared'
import { useServerFileUploadLimit } from '~~/lib/common/composables/serverInfo'
import type {
UploadableFileItem,
UploadFileItem
} from '~~/lib/form/composables/fileUpload'
import { importFile } from '~~/lib/core/api/fileImport'
import { importFileLegacy, type ImportFile } from '~~/lib/core/api/fileImport'
import { useAuthCookie } from '~~/lib/auth/composables/auth'
import { BlobUploadStatus } from '~~/lib/core/api/blobStorage'
import { BlobUploadStatus, type BlobPostResultItem } from '~~/lib/core/api/blobStorage'
import { useMixpanel } from '~~/lib/core/composables/mp'
import { graphql } from '~/lib/common/generated/gql'
import type {
UseFileImport_ModelFragment,
UseFileImport_ProjectFragment
} from '~/lib/common/generated/gql/graphql'
import { useApolloClient } from '@vue/apollo-composable'
const generateUploadUrlMutation = graphql(`
mutation GenerateUploadUrl($input: GenerateFileUploadUrlInput!) {
fileUploadMutations {
generateUploadUrl(input: $input) {
url
fileId
}
}
}
`)
const startFileImportMutation = graphql(`
mutation StartFileImport($input: StartFileImportInput!) {
fileUploadMutations {
startFileImport(input: $input) {
id
}
}
}
`)
export const useFileImportApi = () => {
const {
public: { FF_LARGE_FILE_IMPORTS_ENABLED }
} = useRuntimeConfig()
const apollo = useApolloClient().client
const importFileV2: ImportFile = async (params, callbacks) => {
const { file, projectId, modelId } = params
const { onProgress } = callbacks || {}
// Generate upload URL
const generateUploadUrlResponse = await apollo.mutate({
mutation: generateUploadUrlMutation,
variables: {
input: {
projectId,
fileName: file.name
}
}
})
const generateUploadUrl =
generateUploadUrlResponse.data?.fileUploadMutations.generateUploadUrl
if (!generateUploadUrl) {
const errMsg = getFirstGqlErrorMessage(
generateUploadUrlResponse.errors,
"Couldn't generate upload URL"
)
throw new Error(errMsg)
}
const { url: uploadUrl, fileId } = generateUploadUrl
// Upload to S3 compatible endpoint
const request = new XMLHttpRequest()
const uploadPromise = buildManualPromise<{ etag: string }>()
request.open('PUT', uploadUrl)
request.setRequestHeader('Content-Type', file.type)
request.upload.addEventListener('progress', (e) => {
const percentage = (e.loaded / e.total) * 100
onProgress?.(percentage)
})
const handleResponse = () => {
const statusCode = request.status
if (statusCode >= 200 && statusCode < 300) {
// Collect etag
const etag = request.getResponseHeader('ETag')
if (!etag) {
return uploadPromise.reject(new Error('No ETag in upload response'))
}
return uploadPromise.resolve({ etag })
} else {
// Try to resolve error message from XML response w/ regex (dont want to parse XML)
const errorMessage = request.responseText.match(
/<Message>(.*?)<\/Message>/
)?.[1]
return uploadPromise.reject(
new Error(errorMessage || `Upload failed with status ${statusCode}`)
)
}
}
request.addEventListener('load', () => handleResponse())
request.addEventListener('error', () => handleResponse())
request.send(file)
const { etag } = await uploadPromise.promise
// Now lets start the file import
const startFileImportResponse = await apollo.mutate({
mutation: startFileImportMutation,
variables: {
input: {
projectId,
fileId,
etag,
modelId
}
}
})
const fileImportStarted =
startFileImportResponse.data?.fileUploadMutations.startFileImport.id
if (!fileImportStarted) {
const errMsg = getFirstGqlErrorMessage(
startFileImportResponse.errors,
"Couldn't start file import"
)
throw new Error(errMsg)
}
const res: BlobPostResultItem = {
fileName: file.name,
fileSize: file.size,
formKey: 'file',
uploadStatus: BlobUploadStatus.Completed,
uploadError: ''
}
return res
}
return {
importFile: FF_LARGE_FILE_IMPORTS_ENABLED ? importFileV2 : importFileLegacy
}
}
graphql(`
fragment UseFileImport_Project on Project {
@@ -31,12 +160,11 @@ graphql(`
export function useFileImport(params: {
project: MaybeRef<UseFileImport_ProjectFragment>
model?: MaybeRef<MaybeNullOrUndefined<UseFileImport_ModelFragment>>
/**
* Sometimes we don't have a model, but we still want to specify a target model name (e.g. for
* model list view uploads, where list items don't necessarily represent real models)
* Model should exist if upload is automatically triggered. Otherwise you must still feed it in, but
* at the point when you call uploadSelected().
*/
modelName?: MaybeRef<MaybeNullOrUndefined<string>>
model?: MaybeRef<MaybeNullOrUndefined<UseFileImport_ModelFragment>>
/**
* If true, the upload will be prepared and validated, but for it to start you must invoke uploadSelected() manually
*/
@@ -58,18 +186,19 @@ export function useFileImport(params: {
fileSelectedCallback
} = params
const { importFile } = useFileImportApi()
const { maxSizeInBytes } = useServerFileUploadLimit()
const authToken = useAuthCookie()
const apiOrigin = useApiOrigin()
const accept = ref('.ifc,.stl,.obj')
const upload = ref(null as Nullable<UploadFileItem & { modelName: Optional<string> }>)
const upload = ref(null as Nullable<UploadFileItem>)
const isUploading = ref(false)
const modelName = computed(() => unref(params.modelName) || unref(model)?.name)
const isUploadable = computed(() => {
if (!upload.value) return false
if (upload.value.error) return false
if (upload.value.result) return false
if (isUploading.value) return false
if (!authToken.value) return false
if (!upload.value.file) return false
@@ -80,20 +209,32 @@ export function useFileImport(params: {
const uploadSelected = async (params?: {
/**
* Optionally override model name to target for the upload
* Optionally override model target for the upload
*/
modelName?: string
model: UseFileImport_ModelFragment
}) => {
if (!isUploadable.value || !upload.value || !authToken.value) return
const finalModelName = params?.modelName || upload.value.modelName
const baseModel = unref(model)
const overridenModel = params?.model
isUploading.value = true
try {
let finalModel: UseFileImport_ModelFragment
if (overridenModel) {
finalModel = overridenModel
} else if (baseModel) {
finalModel = baseModel
} else {
throw new Error('No model provided for file import')
}
const res = await importFile(
{
file: upload.value.file,
projectId: unref(project).id,
modelName: finalModelName,
modelName: finalModel.name,
modelId: finalModel.id,
authToken: authToken.value,
apiOrigin
},
@@ -104,13 +245,11 @@ export function useFileImport(params: {
}
)
upload.value.result = res
// TODO: add file extension
// const extension = res.fileName?.split('.').reverse()[0]
mp.track('Upload Action', {
type: 'action',
name: 'create',
source: finalModelName ? 'model card' : 'empty card'
// extension
source: 'model card'
})
fileUploadedCallback?.(upload.value)
@@ -131,13 +270,7 @@ export function useFileImport(params: {
upload.value = null
}
const onFilesSelected = async (params: {
files: UploadableFileItem[]
/**
* Optionally override model name to target for the upload
*/
modelName?: string
}) => {
const onFilesSelected = async (params: { files: UploadableFileItem[] }) => {
if (isUploading.value || !authToken.value) return
const file = params.files[0]
@@ -146,8 +279,7 @@ export function useFileImport(params: {
upload.value = {
...file,
result: undefined,
progress: 0,
modelName: params.modelName || modelName.value || undefined
progress: 0
}
if (file.error) {
@@ -1,3 +1,7 @@
import type { UploadFileItem } from '@speckle/ui-components'
export type FileAreaUploadingPayload = { isUploading: boolean; upload: UploadFileItem }
export type FileAreaUploadingPayload = {
isUploading: boolean
upload: UploadFileItem
error: string | null
}
@@ -10,6 +10,7 @@ import type { FileTypeSpecifier } from '~~/src/helpers/form/file'
import { computed, unref } from 'vue'
import type { CSSProperties } from 'vue'
import { BaseError } from '~~/src/lib'
import type { BlobUploadStatus } from '@speckle/shared/blobs'
/**
* A file, as emitted out from FileUploadZone
@@ -31,7 +32,7 @@ export type BlobPostResultItem = {
/**
* Success = 1, Failure = 2
*/
uploadStatus: number
uploadStatus: BlobUploadStatus
uploadError: string
}