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 @@
-
+
@@ -54,6 +54,10 @@ defineProps<{
* Whether to allow retry and show a retry button when loading fails
*/
allowRetry?: boolean
+ /**
+ * Hide completely and prevent taking any space when not loading
+ */
+ hideWhenComplete?: boolean
}>()
const wrapper = ref(null as Nullable
)
diff --git a/packages/ui-components/src/components/layout/Table.stories.ts b/packages/ui-components/src/components/layout/Table.stories.ts
index 086b50ce1..cb70d824f 100644
--- a/packages/ui-components/src/components/layout/Table.stories.ts
+++ b/packages/ui-components/src/components/layout/Table.stories.ts
@@ -20,15 +20,17 @@ export default {
}
} as Meta
-export const Default: StoryObj = {
- render: (args) => ({
- components: { Table, ShieldCheckIcon, ShieldExclamationIcon, TrashIcon },
- setup() {
- return { args }
- },
- template: `
+const buildRender = (
+ options?: Partial<{
+ wrapTemplate: (core: string) => string
+ style?: string
+ }>
+): StoryObj['render'] => {
+ const { wrapTemplate = (val: string) => val } = options || {}
+ const template = wrapTemplate(`
@@ -64,8 +66,19 @@ export const Default: StoryObj = {
- `
- }),
+ `)
+
+ return (args) => ({
+ components: { Table, ShieldCheckIcon, ShieldExclamationIcon, TrashIcon },
+ setup() {
+ return { args }
+ },
+ template
+ })
+}
+
+export const Default: StoryObj = {
+ render: buildRender(),
args: {
columns: [
{ id: 'name', header: 'Name', classes: 'col-span-3 truncate' },
@@ -226,10 +239,12 @@ export const NoItems: StoryObj = {
}
}
-export const WithLimitedHeight: StoryObj = {
- ...Default,
+export const WithLimitedSpace: StoryObj = {
+ render: buildRender({
+ style: 'width: 400px; height: 200px;'
+ }),
args: {
- ...Default.args,
- maxHeight: 200
+ ...Default.args
+ // maxHeight: 200
}
}
diff --git a/packages/ui-components/src/components/layout/Table.vue b/packages/ui-components/src/components/layout/Table.vue
index 8f1f1364b..a2b64c6a4 100644
--- a/packages/ui-components/src/components/layout/Table.vue
+++ b/packages/ui-components/src/components/layout/Table.vue
@@ -1,85 +1,81 @@
-
-
-
-
- {{ column.header }}
-
+
+
+
+ {{ column.header }}
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
- {{ emptyMessage }}
-
-
+
+
+
+
-
+
+
+
+
+
+ {{ emptyMessage }}
+
+
+
+