diff --git a/packages/frontend-2/components/preview/Image.vue b/packages/frontend-2/components/preview/Image.vue index 972e61339..6896bb527 100644 --- a/packages/frontend-2/components/preview/Image.vue +++ b/packages/frontend-2/components/preview/Image.vue @@ -109,7 +109,8 @@ const { panoramaPreviewUrl, shouldLoadPanorama, isLoadingPanorama, - hasDoneFirstLoad + hasDoneFirstLoad, + isPanoramaPlaceholder } = usePreviewImageBlob(basePreviewUrl, { enabled: isInViewport }) const hovered = ref(false) @@ -152,10 +153,15 @@ const shouldShowMainPreview = computed( () => (!hovered.value && finalPreviewUrl.value) || isLoadingPanorama.value || - !props.panoramaOnHover + !props.panoramaOnHover || + isPanoramaPlaceholder.value ) const shouldShowPanoramicPreview = computed( - () => hovered.value && panoramaPreviewUrl.value && props.panoramaOnHover + () => + hovered.value && + panoramaPreviewUrl.value && + props.panoramaOnHover && + !isPanoramaPlaceholder.value ) onMounted(() => setParentDimensions()) diff --git a/packages/frontend-2/components/project/CardImportFileArea.vue b/packages/frontend-2/components/project/CardImportFileArea.vue index 4e634fabb..432cf2548 100644 --- a/packages/frontend-2/components/project/CardImportFileArea.vue +++ b/packages/frontend-2/components/project/CardImportFileArea.vue @@ -29,8 +29,8 @@ {{ errorMessage }}
@@ -67,17 +67,14 @@ diff --git a/packages/frontend-2/lib/core/composables/fileImport.ts b/packages/frontend-2/lib/core/composables/fileImport.ts index bcb7b82b5..86a175e29 100644 --- a/packages/frontend-2/lib/core/composables/fileImport.ts +++ b/packages/frontend-2/lib/core/composables/fileImport.ts @@ -37,52 +37,55 @@ export function useFileImport(params: { * model list view uploads, where list items don't necessarily represent real models) */ modelName?: MaybeRef> + /** + * If true, the upload will be prepared and validated, but for it to start you must invoke uploadSelected() manually + */ + manuallyTriggerUpload?: boolean + /** + * Optionally handle the file upload completion event. + */ + fileUploadedCallback?: Optional<(file: UploadFileItem) => void> + /** + * Optionally handle the file selection event. + */ + fileSelectedCallback?: Optional<() => void> }) { - const { project, model } = params + const { + project, + model, + manuallyTriggerUpload, + fileUploadedCallback, + fileSelectedCallback + } = params const { maxSizeInBytes } = useServerFileUploadLimit() const authToken = useAuthCookie() const apiOrigin = useApiOrigin() const accept = ref('.ifc,.stl,.obj') - const upload = ref(null as Nullable) + const upload = ref(null as Nullable }>) const isUploading = ref(false) const modelName = computed(() => unref(params.modelName) || unref(model)?.name) - - let onFileUploadedCb: Optional<(file: UploadFileItem) => void> = undefined - const onFileUploaded = (cb: (file: UploadFileItem) => void) => { - onFileUploadedCb = cb - } + const isUploadable = computed(() => { + if (!upload.value) return false + if (upload.value.error) return false + if (isUploading.value) return false + if (!authToken.value) return false + if (!upload.value.file) return false + return true + }) const mp = useMixpanel() - const onFilesSelected = async (params: { - files: UploadableFileItem[] + + const uploadSelected = async (params?: { /** * Optionally override model name to target for the upload */ modelName?: string }) => { - if (isUploading.value || !authToken.value) return - - const file = params.files[0] - if (!file) return - - upload.value = { - ...file, - result: undefined, - progress: 0 - } - - if (file.error) { - return - } - - upload.value = { - ...file, - result: undefined, - progress: 0 - } + if (!isUploadable.value || !upload.value || !authToken.value) return + const finalModelName = params?.modelName || upload.value.modelName isUploading.value = true try { @@ -90,7 +93,7 @@ export function useFileImport(params: { { file: upload.value.file, projectId: unref(project).id, - modelName: params.modelName || modelName.value || undefined, + modelName: finalModelName, authToken: authToken.value, apiOrigin }, @@ -106,11 +109,11 @@ export function useFileImport(params: { mp.track('Upload Action', { type: 'action', name: 'create', - source: modelName.value ? 'model card' : 'empty card' + source: finalModelName ? 'model card' : 'empty card' // extension }) - onFileUploadedCb?.(upload.value) + fileUploadedCallback?.(upload.value) } catch (e) { upload.value.result = { uploadStatus: BlobUploadStatus.Error, @@ -123,12 +126,48 @@ export function useFileImport(params: { } } + const resetSelected = () => { + if (isUploading.value) return + upload.value = null + } + + const onFilesSelected = async (params: { + files: UploadableFileItem[] + /** + * Optionally override model name to target for the upload + */ + modelName?: string + }) => { + if (isUploading.value || !authToken.value) return + + const file = params.files[0] + if (!file) return + + upload.value = { + ...file, + result: undefined, + progress: 0, + modelName: params.modelName || modelName.value || undefined + } + + if (file.error) { + return + } + + fileSelectedCallback?.() + if (!manuallyTriggerUpload) { + await uploadSelected() + } + } + return { maxSizeInBytes, onFilesSelected, accept, upload, isUploading, - onFileUploaded + uploadSelected, + resetSelected, + isUploadable } } diff --git a/packages/frontend-2/lib/form/composables/fileUpload.ts b/packages/frontend-2/lib/form/composables/fileUpload.ts index 11bf540f1..883504c04 100644 --- a/packages/frontend-2/lib/form/composables/fileUpload.ts +++ b/packages/frontend-2/lib/form/composables/fileUpload.ts @@ -35,7 +35,7 @@ export function useFileUploadProgressCore(params: { const progressBarStyle = computed((): CSSProperties => { const item = unref(params.item) return { - width: `${item ? item.progress : 0}%` + width: `${Math.max(item ? item.progress : 0, 1)}%` } }) diff --git a/packages/frontend-2/lib/form/helpers/fileUpload.ts b/packages/frontend-2/lib/form/helpers/fileUpload.ts new file mode 100644 index 000000000..862bb7994 --- /dev/null +++ b/packages/frontend-2/lib/form/helpers/fileUpload.ts @@ -0,0 +1,3 @@ +import type { UploadFileItem } from '@speckle/ui-components' + +export type FileAreaUploadingPayload = { isUploading: boolean; upload: UploadFileItem } diff --git a/packages/frontend-2/lib/projects/composables/previewImage.ts b/packages/frontend-2/lib/projects/composables/previewImage.ts index f3d65a087..5b53ad054 100644 --- a/packages/frontend-2/lib/projects/composables/previewImage.ts +++ b/packages/frontend-2/lib/projects/composables/previewImage.ts @@ -35,13 +35,15 @@ export function usePreviewImageBlob( const basePanoramaUrl = computed(() => unref(previewUrl) + '/all') const isEnabled = computed(() => (import.meta.server ? true : unref(enabled))) const cacheBust = ref(0) + const isPanoramaPlaceholder = ref(false) const ret = { previewUrl: computed(() => url.value), panoramaPreviewUrl: computed(() => panoramaUrl.value), isLoadingPanorama, shouldLoadPanorama, - hasDoneFirstLoad: computed(() => hasDoneFirstLoad.value) + hasDoneFirstLoad: computed(() => hasDoneFirstLoad.value), + isPanoramaPlaceholder: computed(() => isPanoramaPlaceholder.value) } // Preload the image @@ -168,6 +170,9 @@ export function usePreviewImageBlob( img.onload = resolve img.onerror = reject }) + + // If width is 700px or less, it's the placeholder not the actual panorama + isPanoramaPlaceholder.value = img.naturalWidth <= 700 } panoramaUrl.value = blobUrl diff --git a/packages/ui-components/src/components/InfiniteLoading.vue b/packages/ui-components/src/components/InfiniteLoading.vue index ec2cd7bff..5203e9635 100644 --- a/packages/ui-components/src/components/InfiniteLoading.vue +++ b/packages/ui-components/src/components/InfiniteLoading.vue @@ -10,7 +10,7 @@